18e381ea Andy McCall 1 month ago ↶
1
2
3
use super :: { now_ms, Pool} ;
4
use sqlx:: FromRow;
5
6
# [ derive ( Clone, Debug, FromRow ) ]
7
pub struct IssueRow {
8
pub id : i64 ,
9
pub repo_id : i64 ,
10
pub number : i64 ,
11
pub title : String,
12
pub body : String,
13
pub author_id : Option< i64 > ,
14
pub author : String,
15
pub state : String,
16
pub reason : String,
17
pub locked : bool ,
18
pub pinned : bool ,
19
pub milestone_id : Option< i64 > ,
20
pub created_ms : i64 ,
21
pub updated_ms : i64 ,
22
pub closed_ms : Option< i64 > ,
23
}
24
25
# [ derive ( Clone, Debug, FromRow ) ]
26
pub struct IssueComment {
27
pub id : i64 ,
28
pub issue_id : i64 ,
29
pub author_id : Option< i64 > ,
30
pub author : String,
31
pub body : String,
32
pub created_ms : i64 ,
33
pub edited_ms : Option< i64 > ,
34
}
35
36
# [ derive ( Clone, Debug, FromRow ) ]
37
pub struct IssueEvent {
38
pub id : i64 ,
39
pub at_ms : i64 ,
40
pub actor : String,
41
pub event : String,
42
pub detail : String,
43
}
44
45
# [ derive ( Clone, Debug, FromRow, PartialEq, Eq ) ]
46
pub struct Label {
47
pub id : i64 ,
48
pub repo_id : i64 ,
49
pub name : String,
50
pub colour : String,
51
pub description : String,
52
}
53
54
# [ derive ( Clone, Debug, FromRow ) ]
55
pub struct Milestone {
56
pub id : i64 ,
57
pub repo_id : i64 ,
58
pub title : String,
59
pub description : String,
60
pub due_ms : Option< i64 > ,
61
pub state : String,
62
}
63
64
pub struct NewIssue < 'a > {
65
pub repo_id : i64 ,
66
pub title : & 'a str ,
67
pub body : & 'a str ,
68
pub author_id : Option< i64 > ,
69
pub author : & 'a str ,
70
}
71
72
pub async fn create ( pool : & Pool, i : NewIssue< '_ > ) -> sqlx:: Result< IssueRow> {
73
let number = super :: proposals:: next_number( pool, i.repo_id) .await? ;
74
let now = now_ms ( ) ;
5d90e0e0 Andy McCall 20 days ago
75
sqlx:: query_as( " INSERT INTO issues (repo_id, number, title, body, author_id, author, created_ms, updated_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *" )
18e381ea Andy McCall 1 month ago ↶
76
.bind ( i.repo_id)
77
.bind ( number)
78
.bind ( i.title)
79
.bind ( i.body)
80
.bind ( i.author_id)
81
.bind ( i.author)
82
.bind ( now)
83
.bind ( now)
84
.fetch_one ( pool)
85
.await
86
}
87
88
pub async fn get ( pool : & Pool, repo_id : i64 , number : i64 ) -> sqlx:: Result< Option< IssueRow> > {
5d90e0e0 Andy McCall 20 days ago
89
sqlx:: query_as( " SELECT * FROM issues WHERE repo_id = $1 AND number = $2" ) .bind ( repo_id) .bind ( number) .fetch_optional ( pool) .await
18e381ea Andy McCall 1 month ago ↶
90
}
91
92
pub async fn get_by_id ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< IssueRow> > {
5d90e0e0 Andy McCall 20 days ago
93
sqlx:: query_as( " SELECT * FROM issues WHERE id = $1" ) .bind ( id) .fetch_optional ( pool) .await
18e381ea Andy McCall 1 month ago ↶
94
}
95
96
pub async fn list ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< IssueRow> > {
5d90e0e0 Andy McCall 20 days ago
97
sqlx:: query_as( " SELECT * FROM issues WHERE repo_id = $1 ORDER BY pinned DESC, number DESC" ) .bind ( repo_id) .fetch_all ( pool) .await
18e381ea Andy McCall 1 month ago ↶
98
}
99
100
101
pub async fn numbers ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< ( Vec< i64 > , Vec< i64 > ) > {
5d90e0e0 Andy McCall 20 days ago
102
let issues: Vec< i64 > = sqlx:: query_scalar( " SELECT number FROM issues WHERE repo_id = $1" ) .bind ( repo_id) .fetch_all ( pool) .await? ;
103
let proposals: Vec< i64 > = sqlx:: query_scalar( " SELECT number FROM proposals WHERE repo_id = $1" ) .bind ( repo_id) .fetch_all ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
104
Ok ( ( issues, proposals) )
105
}
106
107
pub async fn assigned_to ( pool : & Pool, account_id : i64 ) -> sqlx:: Result< Vec< IssueRow> > {
5d90e0e0 Andy McCall 20 days ago
108
sqlx:: query_as( " SELECT i.* FROM issues i JOIN issue_assignees a ON a.issue_id = i.id WHERE a.account_id = $1 AND i.state = 'open' ORDER BY i.updated_ms DESC" )
18e381ea Andy McCall 1 month ago ↶
109
.bind ( account_id)
110
.fetch_all ( pool)
111
.await
112
}
113
114
pub async fn edit ( pool : & Pool, id : i64 , title : & str , body : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
115
sqlx:: query( " UPDATE issues SET title = $1, body = $2, updated_ms = $3 WHERE id = $4" )
18e381ea Andy McCall 1 month ago ↶
116
.bind ( title)
117
.bind ( body)
118
.bind ( now_ms ( ) )
119
.bind ( id)
120
.execute ( pool)
121
.await? ;
122
Ok ( ( ) )
123
}
124
125
pub async fn set_state ( pool : & Pool, id : i64 , state : & str , reason : & str ) -> sqlx:: Result< ( ) > {
126
let now = now_ms ( ) ;
127
let closed: Option< i64 > = if state = = " closed" { Some ( now) } else { None } ;
5d90e0e0 Andy McCall 20 days ago
128
sqlx:: query( " UPDATE issues SET state = $1, reason = $2, closed_ms = $3, updated_ms = $4 WHERE id = $5" )
18e381ea Andy McCall 1 month ago ↶
129
.bind ( state)
130
.bind ( reason)
131
.bind ( closed)
132
.bind ( now)
133
.bind ( id)
134
.execute ( pool)
135
.await? ;
136
Ok ( ( ) )
137
}
138
139
pub async fn set_flags ( pool : & Pool, id : i64 , locked : Option< bool > , pinned : Option< bool > ) -> sqlx:: Result< ( ) > {
140
if let Some ( l) = locked {
5d90e0e0 Andy McCall 20 days ago
141
sqlx:: query( " UPDATE issues SET locked = $1 WHERE id = $2" ) .bind ( l) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
142
}
143
if let Some ( p) = pinned {
5d90e0e0 Andy McCall 20 days ago
144
sqlx:: query( " UPDATE issues SET pinned = $1 WHERE id = $2" ) .bind ( p) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
145
}
146
Ok ( ( ) )
147
}
148
149
pub async fn set_milestone ( pool : & Pool, id : i64 , milestone_id : Option< i64 > ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
150
sqlx:: query( " UPDATE issues SET milestone_id = $1, updated_ms = $2 WHERE id = $3" )
18e381ea Andy McCall 1 month ago ↶
151
.bind ( milestone_id)
152
.bind ( now_ms ( ) )
153
.bind ( id)
154
.execute ( pool)
155
.await? ;
156
Ok ( ( ) )
157
}
158
159
pub async fn touch ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
160
sqlx:: query( " UPDATE issues SET updated_ms = $1 WHERE id = $2" ) .bind ( now_ms ( ) ) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
161
Ok ( ( ) )
162
}
163
164
pub async fn comments ( pool : & Pool, issue_id : i64 ) -> sqlx:: Result< Vec< IssueComment> > {
5d90e0e0 Andy McCall 20 days ago
165
sqlx:: query_as( " SELECT * FROM issue_comments WHERE issue_id = $1 ORDER BY created_ms, id" ) .bind ( issue_id) .fetch_all ( pool) .await
18e381ea Andy McCall 1 month ago ↶
166
}
167
168
pub async fn add_comment ( pool : & Pool, issue_id : i64 , author_id : Option< i64 > , author : & str , body : & str ) -> sqlx:: Result< IssueComment> {
5d90e0e0 Andy McCall 20 days ago
169
let row = sqlx:: query_as(
170
" INSERT INTO issue_comments (issue_id, author_id, author, body, created_ms) VALUES ($1, $2, $3, $4, $5) RETURNING *" ,
171
)
172
.bind ( issue_id)
173
.bind ( author_id)
174
.bind ( author)
175
.bind ( body)
176
.bind ( now_ms ( ) )
177
.fetch_one ( pool)
178
.await? ;
18e381ea Andy McCall 1 month ago ↶
179
touch ( pool, issue_id) .await? ;
180
Ok ( row)
181
}
182
183
pub async fn edit_comment ( pool : & Pool, id : i64 , author_id : i64 , body : & str ) -> sqlx:: Result< bool > {
5d90e0e0 Andy McCall 20 days ago
184
let n = sqlx:: query( " UPDATE issue_comments SET body = $1, edited_ms = $2 WHERE id = $3 AND author_id = $4" )
18e381ea Andy McCall 1 month ago ↶
185
.bind ( body)
186
.bind ( now_ms ( ) )
187
.bind ( id)
188
.bind ( author_id)
189
.execute ( pool)
190
.await? ;
191
Ok ( n.rows_affected ( ) > 0 )
192
}
193
194
pub async fn delete_comment ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
195
sqlx:: query( " DELETE FROM issue_comments WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
196
Ok ( ( ) )
197
}
198
199
pub async fn events ( pool : & Pool, issue_id : i64 ) -> sqlx:: Result< Vec< IssueEvent> > {
5d90e0e0 Andy McCall 20 days ago
200
sqlx:: query_as( " SELECT id, at_ms, actor, event, detail FROM issue_events WHERE issue_id = $1 ORDER BY at_ms, id" )
18e381ea Andy McCall 1 month ago ↶
201
.bind ( issue_id)
202
.fetch_all ( pool)
203
.await
204
}
205
8bb7baeb Andy McCall 10 days ago ↶
206
207
208
pub async fn referenced_by ( pool : & Pool, issue_id : i64 , hash : & str ) -> sqlx:: Result< bool > {
209
let n: i64 = sqlx:: query_scalar(
210
" SELECT COUNT(*) FROM issue_events WHERE issue_id = $1 AND event = 'referenced by change' AND (detail = $2 OR detail LIKE $3)" ,
211
)
212
.bind ( issue_id)
213
.bind ( hash)
214
.bind ( format! ( " {hash} %" ) )
215
.fetch_one ( pool)
216
.await? ;
217
Ok ( n > 0 )
218
}
219
18e381ea Andy McCall 1 month ago ↶
220
pub async fn add_event ( pool : & Pool, issue_id : i64 , actor : & str , event : & str , detail : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
221
sqlx:: query( " INSERT INTO issue_events (issue_id, at_ms, actor, event, detail) VALUES ($1, $2, $3, $4, $5)" )
18e381ea Andy McCall 1 month ago ↶
222
.bind ( issue_id)
223
.bind ( now_ms ( ) )
224
.bind ( actor)
225
.bind ( event)
226
.bind ( detail)
227
.execute ( pool)
228
.await? ;
229
Ok ( ( ) )
230
}
231
232
233
234
pub async fn labels ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< Label> > {
5d90e0e0 Andy McCall 20 days ago
235
sqlx:: query_as( " SELECT * FROM labels WHERE repo_id = $1 ORDER BY name" ) .bind ( repo_id) .fetch_all ( pool) .await
18e381ea Andy McCall 1 month ago ↶
236
}
237
238
pub async fn label_by_name ( pool : & Pool, repo_id : i64 , name : & str ) -> sqlx:: Result< Option< Label> > {
5d90e0e0 Andy McCall 20 days ago
239
sqlx:: query_as( " SELECT * FROM labels WHERE repo_id = $1 AND name = $2" ) .bind ( repo_id) .bind ( name) .fetch_optional ( pool) .await
18e381ea Andy McCall 1 month ago ↶
240
}
241
242
pub async fn upsert_label ( pool : & Pool, repo_id : i64 , name : & str , colour : & str , description : & str ) -> sqlx:: Result< Label> {
5d90e0e0 Andy McCall 20 days ago
243
sqlx:: query_as( " INSERT INTO labels (repo_id, name, colour, description) VALUES ($1, $2, $3, $4) ON CONFLICT (repo_id, name) DO UPDATE SET colour = excluded.colour, description = excluded.description RETURNING *" )
18e381ea Andy McCall 1 month ago ↶
244
.bind ( repo_id)
245
.bind ( name)
246
.bind ( colour)
247
.bind ( description)
248
.fetch_one ( pool)
249
.await
250
}
251
252
pub async fn delete_label ( pool : & Pool, id : i64 ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
253
sqlx:: query( " DELETE FROM labels WHERE id = $1" ) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
254
Ok ( ( ) )
255
}
256
257
pub async fn issue_labels ( pool : & Pool, issue_id : i64 ) -> sqlx:: Result< Vec< Label> > {
5d90e0e0 Andy McCall 20 days ago
258
sqlx:: query_as( " SELECT l.* FROM labels l JOIN issue_labels il ON il.label_id = l.id WHERE il.issue_id = $1 ORDER BY l.name" )
18e381ea Andy McCall 1 month ago ↶
259
.bind ( issue_id)
260
.fetch_all ( pool)
261
.await
262
}
263
264
pub async fn set_issue_label ( pool : & Pool, issue_id : i64 , label_id : i64 , on : bool ) -> sqlx:: Result< ( ) > {
265
if on {
d1bf2d69 Andy McCall 20 days ago ↶
266
sqlx:: query( " INSERT INTO issue_labels (issue_id, label_id) VALUES ($1, $2) ON CONFLICT DO NOTHING" )
18e381ea Andy McCall 1 month ago ↶
267
.bind ( issue_id)
268
.bind ( label_id)
269
.execute ( pool)
270
.await? ;
271
} else {
5d90e0e0 Andy McCall 20 days ago
272
sqlx:: query( " DELETE FROM issue_labels WHERE issue_id = $1 AND label_id = $2" ) .bind ( issue_id) .bind ( label_id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
273
}
274
Ok ( ( ) )
275
}
276
277
1ac84c62 Andy McCall 1 month ago ↶
278
279
pub async fn labels_by_proposal ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< ( i64 , Label) > > {
280
let rows: Vec< ( i64 , i64 , i64 , String, String, String) > = sqlx:: query_as(
5d90e0e0 Andy McCall 20 days ago
281
" SELECT pl.proposal_id, l.id, l.repo_id, l.name, l.colour, l.description FROM proposal_labels pl JOIN labels l ON l.id = pl.label_id JOIN proposals p ON p.id = pl.proposal_id WHERE p.repo_id = $1 ORDER BY l.name" ,
1ac84c62 Andy McCall 1 month ago ↶
282
)
283
.bind ( repo_id)
284
.fetch_all ( pool)
285
.await? ;
286
Ok ( rows
287
.into_iter ( )
288
.map ( | ( proposal_id , id , repo_id , name , colour , description ) | ( proposal_id, Label { id, repo_id, name, colour, description } ) )
289
.collect ( ) )
290
}
291
18e381ea Andy McCall 1 month ago ↶
292
pub async fn labels_by_issue ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< ( i64 , Label) > > {
293
let rows: Vec< ( i64 , i64 , i64 , String, String, String) > = sqlx:: query_as(
5d90e0e0 Andy McCall 20 days ago
294
" SELECT il.issue_id, l.id, l.repo_id, l.name, l.colour, l.description FROM issue_labels il JOIN labels l ON l.id = il.label_id JOIN issues i ON i.id = il.issue_id WHERE i.repo_id = $1 ORDER BY l.name" ,
18e381ea Andy McCall 1 month ago ↶
295
)
296
.bind ( repo_id)
297
.fetch_all ( pool)
298
.await? ;
299
Ok ( rows
300
.into_iter ( )
301
.map ( | ( issue_id , id , repo_id , name , colour , description ) | ( issue_id, Label { id, repo_id, name, colour, description } ) )
302
.collect ( ) )
303
}
304
305
pub async fn proposal_labels ( pool : & Pool, proposal_id : i64 ) -> sqlx:: Result< Vec< Label> > {
5d90e0e0 Andy McCall 20 days ago
306
sqlx:: query_as( " SELECT l.* FROM labels l JOIN proposal_labels pl ON pl.label_id = l.id WHERE pl.proposal_id = $1 ORDER BY l.name" )
18e381ea Andy McCall 1 month ago ↶
307
.bind ( proposal_id)
308
.fetch_all ( pool)
309
.await
310
}
311
312
pub async fn set_proposal_label ( pool : & Pool, proposal_id : i64 , label_id : i64 , on : bool ) -> sqlx:: Result< ( ) > {
313
if on {
d1bf2d69 Andy McCall 20 days ago ↶
314
sqlx:: query( " INSERT INTO proposal_labels (proposal_id, label_id) VALUES ($1, $2) ON CONFLICT DO NOTHING" )
18e381ea Andy McCall 1 month ago ↶
315
.bind ( proposal_id)
316
.bind ( label_id)
317
.execute ( pool)
318
.await? ;
319
} else {
5d90e0e0 Andy McCall 20 days ago
320
sqlx:: query( " DELETE FROM proposal_labels WHERE proposal_id = $1 AND label_id = $2" )
18e381ea Andy McCall 1 month ago ↶
321
.bind ( proposal_id)
322
.bind ( label_id)
323
.execute ( pool)
324
.await? ;
325
}
326
Ok ( ( ) )
327
}
328
329
330
331
pub async fn assignees ( pool : & Pool, issue_id : i64 ) -> sqlx:: Result< Vec< String> > {
332
sqlx:: query_scalar(
5d90e0e0 Andy McCall 20 days ago
333
" SELECT a.username FROM accounts a JOIN issue_assignees ia ON ia.account_id = a.id WHERE ia.issue_id = $1 ORDER BY a.username" ,
18e381ea Andy McCall 1 month ago ↶
334
)
335
.bind ( issue_id)
336
.fetch_all ( pool)
337
.await
338
}
339
340
pub async fn set_assignee ( pool : & Pool, issue_id : i64 , account_id : i64 , on : bool ) -> sqlx:: Result< ( ) > {
341
if on {
d1bf2d69 Andy McCall 20 days ago ↶
342
sqlx:: query( " INSERT INTO issue_assignees (issue_id, account_id) VALUES ($1, $2) ON CONFLICT DO NOTHING" )
18e381ea Andy McCall 1 month ago ↶
343
.bind ( issue_id)
344
.bind ( account_id)
345
.execute ( pool)
346
.await? ;
347
} else {
5d90e0e0 Andy McCall 20 days ago
348
sqlx:: query( " DELETE FROM issue_assignees WHERE issue_id = $1 AND account_id = $2" )
18e381ea Andy McCall 1 month ago ↶
349
.bind ( issue_id)
350
.bind ( account_id)
351
.execute ( pool)
352
.await? ;
353
}
354
Ok ( ( ) )
355
}
356
357
358
pub async fn assignees_by_issue ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< ( i64 , String) > > {
5d90e0e0 Andy McCall 20 days ago
359
sqlx:: query_as( " SELECT ia.issue_id, a.username FROM issue_assignees ia JOIN accounts a ON a.id = ia.account_id JOIN issues i ON i.id = ia.issue_id WHERE i.repo_id = $1" )
18e381ea Andy McCall 1 month ago ↶
360
.bind ( repo_id)
361
.fetch_all ( pool)
362
.await
363
}
364
365
366
367
pub async fn milestones ( pool : & Pool, repo_id : i64 ) -> sqlx:: Result< Vec< Milestone> > {
5d90e0e0 Andy McCall 20 days ago
368
sqlx:: query_as( " SELECT * FROM milestones WHERE repo_id = $1 ORDER BY state, due_ms, title" ) .bind ( repo_id) .fetch_all ( pool) .await
18e381ea Andy McCall 1 month ago ↶
369
}
370
371
pub async fn milestone ( pool : & Pool, id : i64 ) -> sqlx:: Result< Option< Milestone> > {
5d90e0e0 Andy McCall 20 days ago
372
sqlx:: query_as( " SELECT * FROM milestones WHERE id = $1" ) .bind ( id) .fetch_optional ( pool) .await
18e381ea Andy McCall 1 month ago ↶
373
}
374
375
pub async fn upsert_milestone ( pool : & Pool, repo_id : i64 , title : & str , description : & str , due_ms : Option< i64 > ) -> sqlx:: Result< Milestone> {
5d90e0e0 Andy McCall 20 days ago
376
sqlx:: query_as( " INSERT INTO milestones (repo_id, title, description, due_ms) VALUES ($1, $2, $3, $4) ON CONFLICT (repo_id, title) DO UPDATE SET description = excluded.description, due_ms = excluded.due_ms RETURNING *" )
18e381ea Andy McCall 1 month ago ↶
377
.bind ( repo_id)
378
.bind ( title)
379
.bind ( description)
380
.bind ( due_ms)
381
.fetch_one ( pool)
382
.await
383
}
384
5ea4872f Andy McCall 1 month ago ↶
385
386
pub async fn update_milestone ( pool : & Pool, id : i64 , title : & str , description : & str , due_ms : Option< i64 > ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
387
sqlx:: query( " UPDATE milestones SET title = $1, description = $2, due_ms = $3 WHERE id = $4" )
5ea4872f Andy McCall 1 month ago ↶
388
.bind ( title)
389
.bind ( description)
390
.bind ( due_ms)
391
.bind ( id)
392
.execute ( pool)
393
.await? ;
394
Ok ( ( ) )
395
}
396
18e381ea Andy McCall 1 month ago ↶
397
pub async fn set_milestone_state ( pool : & Pool, id : i64 , state : & str ) -> sqlx:: Result< ( ) > {
5d90e0e0 Andy McCall 20 days ago
398
sqlx:: query( " UPDATE milestones SET state = $1 WHERE id = $2" ) .bind ( state) .bind ( id) .execute ( pool) .await? ;
18e381ea Andy McCall 1 month ago ↶
399
Ok ( ( ) )
400
}
401
402
403
pub async fn milestone_counts ( pool : & Pool, milestone_id : i64 ) -> sqlx:: Result< ( i64 , i64 ) > {
5d90e0e0 Andy McCall 20 days ago
404
let open: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM issues WHERE milestone_id = $1 AND state = 'open'" )
18e381ea Andy McCall 1 month ago ↶
405
.bind ( milestone_id)
406
.fetch_one ( pool)
407
.await? ;
5d90e0e0 Andy McCall 20 days ago
408
let closed: i64 = sqlx:: query_scalar( " SELECT COUNT(*) FROM issues WHERE milestone_id = $1 AND state = 'closed'" )
18e381ea Andy McCall 1 month ago ↶
409
.bind ( milestone_id)
410
.fetch_one ( pool)
411
.await? ;
412
Ok ( ( open, closed) )
413
}