| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | | use super::{now_ms, Pool}; |
| 5 | | use sqlx::FromRow; |
| 6 | |
|
| 7 | | #[derive(Clone, Debug, Default, FromRow)] |
| 8 | | pub struct ProposalRow { |
| 9 | | pub id: i64, |
| 10 | | pub repo_id: i64, |
| 11 | | pub number: i64, |
| 12 | | pub title: String, |
| 13 | | pub body: String, |
| 14 | | pub author_id: Option<i64>, |
| 15 | | pub author: String, |
| 16 | | pub state: String, |
| 17 | | pub target: String, |
| 18 | | pub source_kind: String, |
| 19 | | pub source_repo_id: Option<i64>, |
| 20 | | pub source_bookmark: String, |
| 21 | | pub source_changes: String, |
| 22 | | pub projected_state: String, |
| 23 | | pub projected_heads: String, |
| 24 | | pub projected_target: String, |
| 25 | | pub projected_changes: String, |
| 26 | | pub conflicted: bool, |
| 27 | | pub landed_state: String, |
| 28 | | pub landed_by: String, |
| 29 | | pub landed_via: String, |
| 30 | | pub landed_ms: i64, |
| 31 | | pub created_ms: i64, |
| 32 | | pub updated_ms: i64, |
| 33 | | |
| 34 | | #[sqlx(default)] |
| 35 | | pub auto_land_by: String, |
| 36 | | #[sqlx(default)] |
| 37 | | pub auto_land_ms: Option<i64>, |
| 38 | | } |
| 39 | |
|
| 40 | | impl ProposalRow { |
| 41 | 11× | pub fn projected_heads(&self) -> Vec<String> { |
| 42 | 14× | self.projected_heads.split(',').filter(|s| !s.is_empty()).map(String::from).collect() |
| 43 | 11× | } |
| 44 | |
|
| 45 | 41× | pub fn projected_changes(&self) -> Vec<String> { |
| 46 | 41× | self.projected_changes.split(',').filter(|s| !s.is_empty()).map(String::from).collect() |
| 47 | 41× | } |
| 48 | |
|
| 49 | 0× | pub fn source_changes(&self) -> Vec<String> { |
| 50 | 0× | self.source_changes.split(',').filter(|s| !s.is_empty()).map(String::from).collect() |
| 51 | 0× | } |
| 52 | |
|
| 53 | 1149× | pub fn is_open(&self) -> bool { |
| 54 | 1149× | self.state == "open" || self.state == "draft" |
| 55 | 1149× | } |
| 56 | | } |
| 57 | |
|
| 58 | | #[derive(Clone, Debug, FromRow)] |
| 59 | | pub struct CommentRow { |
| 60 | | pub id: i64, |
| 61 | | pub proposal_id: i64, |
| 62 | | pub author_id: Option<i64>, |
| 63 | | pub author: String, |
| 64 | | pub body: String, |
| 65 | | pub path: String, |
| 66 | | pub anchor_change: String, |
| 67 | | pub anchor_index: i64, |
| 68 | | pub resolved: bool, |
| 69 | | pub created_ms: i64, |
| 70 | | |
| 71 | | |
| 72 | | #[sqlx(default)] |
| 73 | | pub via: String, |
| 74 | | } |
| 75 | |
|
| 76 | | #[derive(Clone, Debug, FromRow)] |
| 77 | | pub struct ReviewRow { |
| 78 | | pub id: i64, |
| 79 | | pub proposal_id: i64, |
| 80 | | |
| 81 | | pub author_id: Option<i64>, |
| 82 | | pub author: String, |
| 83 | | pub verdict: String, |
| 84 | | pub body: String, |
| 85 | | pub created_ms: i64, |
| 86 | | |
| 87 | | |
| 88 | | #[sqlx(default)] |
| 89 | | pub via: String, |
| 90 | | } |
| 91 | |
|
| 92 | | #[derive(Clone, Debug, FromRow)] |
| 93 | | pub struct EventRow { |
| 94 | | pub id: i64, |
| 95 | | pub at_ms: i64, |
| 96 | | pub actor: String, |
| 97 | | pub event: String, |
| 98 | | pub detail: String, |
| 99 | | } |
| 100 | |
|
| 101 | |
|
| 102 | 113× | pub async fn next_number(pool: &Pool, repo_id: i64) -> sqlx::Result<i64> { |
| 103 | 113× | let mut tx = super::begin_write(pool).await?; |
| 104 | 113× | sqlx::query("INSERT INTO numbers (repo_id, next) VALUES ($1, 1) ON CONFLICT DO NOTHING").bind(repo_id).execute(&mut *tx).await?; |
| 105 | 113× | let n: i64 = sqlx::query_scalar("SELECT next FROM numbers WHERE repo_id = $1").bind(repo_id).fetch_one(&mut *tx).await?; |
| 106 | 113× | sqlx::query("UPDATE numbers SET next = next + 1 WHERE repo_id = $1").bind(repo_id).execute(&mut *tx).await?; |
| 107 | 113× | tx.commit().await?; |
| 108 | 113× | Ok(n) |
| 109 | 113× | } |
| 110 | |
|
| 111 | | pub struct NewProposal<'a> { |
| 112 | | pub repo_id: i64, |
| 113 | | pub title: &'a str, |
| 114 | | pub body: &'a str, |
| 115 | | pub author_id: Option<i64>, |
| 116 | | pub author: &'a str, |
| 117 | | pub draft: bool, |
| 118 | | pub target: &'a str, |
| 119 | | pub source_kind: &'a str, |
| 120 | | pub source_repo_id: Option<i64>, |
| 121 | | pub source_bookmark: &'a str, |
| 122 | | pub source_changes: &'a str, |
| 123 | | } |
| 124 | |
|
| 125 | 47× | pub async fn create(pool: &Pool, p: NewProposal<'_>) -> sqlx::Result<ProposalRow> { |
| 126 | 47× | let number = next_number(pool, p.repo_id).await?; |
| 127 | 47× | let now = now_ms(); |
| 128 | 47× | sqlx::query_as( |
| 129 | 47× | "INSERT INTO proposals (repo_id, number, title, body, author_id, author, state, target, source_kind, source_repo_id, source_bookmark, source_changes, created_ms, updated_ms) \ |
| 130 | 47× | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *", |
| 131 | | ) |
| 132 | 47× | .bind(p.repo_id) |
| 133 | 47× | .bind(number) |
| 134 | 47× | .bind(p.title) |
| 135 | 47× | .bind(p.body) |
| 136 | 47× | .bind(p.author_id) |
| 137 | 47× | .bind(p.author) |
| 138 | 47× | .bind(if p.draft { "draft" } else { "open" }) |
| 139 | 47× | .bind(p.target) |
| 140 | 47× | .bind(p.source_kind) |
| 141 | 47× | .bind(p.source_repo_id) |
| 142 | 47× | .bind(p.source_bookmark) |
| 143 | 47× | .bind(p.source_changes) |
| 144 | 47× | .bind(now) |
| 145 | 47× | .bind(now) |
| 146 | 47× | .fetch_one(pool) |
| 147 | 47× | .await |
| 148 | 47× | } |
| 149 | |
|
| 150 | 299× | pub async fn get(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Option<ProposalRow>> { |
| 151 | 299× | sqlx::query_as("SELECT * FROM proposals WHERE repo_id = $1 AND number = $2").bind(repo_id).bind(number).fetch_optional(pool).await |
| 152 | 299× | } |
| 153 | |
|
| 154 | 752× | pub async fn list(pool: &Pool, repo_id: i64, states: &[&str]) -> sqlx::Result<Vec<ProposalRow>> { |
| 155 | 752× | let all: Vec<ProposalRow> = |
| 156 | 752× | sqlx::query_as("SELECT * FROM proposals WHERE repo_id = $1 ORDER BY number DESC").bind(repo_id).fetch_all(pool).await?; |
| 157 | 752× | Ok(all.into_iter().filter(|p| states.is_empty() || states.contains(&p.state.as_str())).collect()) |
| 158 | 752× | } |
| 159 | |
|
| 160 | |
|
| 161 | 0× | pub async fn open_for_author(pool: &Pool, author_id: i64) -> sqlx::Result<Vec<ProposalRow>> { |
| 162 | 0× | sqlx::query_as("SELECT * FROM proposals WHERE author_id = $1 AND state IN ('open', 'draft') ORDER BY updated_ms DESC") |
| 163 | 0× | .bind(author_id) |
| 164 | 0× | .fetch_all(pool) |
| 165 | 0× | .await |
| 166 | 0× | } |
| 167 | |
|
| 168 | 153× | pub async fn set_projection( |
| 169 | 153× | pool: &Pool, |
| 170 | 153× | id: i64, |
| 171 | 153× | state: &str, |
| 172 | 153× | heads: &str, |
| 173 | 153× | target_state: &str, |
| 174 | 153× | changes: &str, |
| 175 | 153× | conflicted: bool, |
| 176 | 153× | ) -> sqlx::Result<()> { |
| 177 | 153× | sqlx::query("UPDATE proposals SET projected_state = $1, projected_heads = $2, projected_target = $3, projected_changes = $4, conflicted = $5 WHERE id = $6") |
| 178 | 153× | .bind(state) |
| 179 | 153× | .bind(heads) |
| 180 | 153× | .bind(target_state) |
| 181 | 153× | .bind(changes) |
| 182 | 153× | .bind(conflicted) |
| 183 | 153× | .bind(id) |
| 184 | 153× | .execute(pool) |
| 185 | 153× | .await?; |
| 186 | 153× | Ok(()) |
| 187 | 153× | } |
| 188 | |
|
| 189 | 9× | pub async fn set_state(pool: &Pool, id: i64, state: &str, landed_state: &str) -> sqlx::Result<()> { |
| 190 | 9× | sqlx::query("UPDATE proposals SET state = $1, landed_state = $2, updated_ms = $3 WHERE id = $4") |
| 191 | 9× | .bind(state) |
| 192 | 9× | .bind(landed_state) |
| 193 | 9× | .bind(now_ms()) |
| 194 | 9× | .bind(id) |
| 195 | 9× | .execute(pool) |
| 196 | 9× | .await?; |
| 197 | 9× | if state == "closed" || state == "landed" { |
| 198 | 3× | super::issues::finished(pool, "proposals", id).await?; |
| 199 | 6× | } |
| 200 | 9× | Ok(()) |
| 201 | 9× | } |
| 202 | |
|
| 203 | |
|
| 204 | 21× | pub async fn set_landed(pool: &Pool, id: i64, landed_state: &str, by: &str, via: &str) -> sqlx::Result<()> { |
| 205 | 21× | let now = now_ms(); |
| 206 | 21× | sqlx::query( |
| 207 | 21× | "UPDATE proposals SET state = 'landed', landed_state = $1, landed_by = $2, landed_via = $3, landed_ms = $4, updated_ms = $5 WHERE id = $6", |
| 208 | 21× | ) |
| 209 | 21× | .bind(landed_state) |
| 210 | 21× | .bind(by) |
| 211 | 21× | .bind(via) |
| 212 | 21× | .bind(now) |
| 213 | 21× | .bind(now) |
| 214 | 21× | .bind(id) |
| 215 | 21× | .execute(pool) |
| 216 | 21× | .await?; |
| 217 | 21× | super::issues::finished(pool, "proposals", id).await |
| 218 | 21× | } |
| 219 | |
|
| 220 | 6× | pub async fn edit(pool: &Pool, id: i64, title: &str, body: &str, target: &str) -> sqlx::Result<()> { |
| 221 | 6× | sqlx::query("UPDATE proposals SET title = $1, body = $2, target = $3, updated_ms = $4 WHERE id = $5") |
| 222 | 6× | .bind(title) |
| 223 | 6× | .bind(body) |
| 224 | 6× | .bind(target) |
| 225 | 6× | .bind(now_ms()) |
| 226 | 6× | .bind(id) |
| 227 | 6× | .execute(pool) |
| 228 | 6× | .await?; |
| 229 | 6× | Ok(()) |
| 230 | 6× | } |
| 231 | |
|
| 232 | 11× | pub async fn touch(pool: &Pool, id: i64) -> sqlx::Result<()> { |
| 233 | 11× | sqlx::query("UPDATE proposals SET updated_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?; |
| 234 | 11× | Ok(()) |
| 235 | 11× | } |
| 236 | |
|
| 237 | 78× | pub async fn comments(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<CommentRow>> { |
| 238 | 78× | sqlx::query_as("SELECT * FROM proposal_comments WHERE proposal_id = $1 ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await |
| 239 | 78× | } |
| 240 | |
|
| 241 | 4× | pub async fn add_comment( |
| 242 | 4× | pool: &Pool, |
| 243 | 4× | proposal_id: i64, |
| 244 | 4× | author_id: Option<i64>, |
| 245 | 4× | author: &str, |
| 246 | 4× | body: &str, |
| 247 | 4× | path: &str, |
| 248 | 4× | anchor: Option<(&str, i64)>, |
| 249 | 4× | ) -> sqlx::Result<CommentRow> { |
| 250 | 4× | let row = sqlx::query_as( |
| 251 | 4× | "INSERT INTO proposal_comments (proposal_id, author_id, author, body, path, anchor_change, anchor_index, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *", |
| 252 | | ) |
| 253 | 4× | .bind(proposal_id) |
| 254 | 4× | .bind(author_id) |
| 255 | 4× | .bind(author) |
| 256 | 4× | .bind(body) |
| 257 | 4× | .bind(path) |
| 258 | 4× | .bind(anchor.map(|a| a.0).unwrap_or("")) |
| 259 | 4× | .bind(anchor.map(|a| a.1).unwrap_or(0)) |
| 260 | 4× | .bind(now_ms()) |
| 261 | 4× | .fetch_one(pool) |
| 262 | 4× | .await?; |
| 263 | 4× | touch(pool, proposal_id).await?; |
| 264 | 4× | Ok(row) |
| 265 | 4× | } |
| 266 | |
|
| 267 | |
|
| 268 | |
|
| 269 | 1× | pub async fn add_comment_via( |
| 270 | 1× | pool: &Pool, |
| 271 | 1× | proposal_id: i64, |
| 272 | 1× | author_id: i64, |
| 273 | 1× | author: &str, |
| 274 | 1× | body: &str, |
| 275 | 1× | via: &str, |
| 276 | 1× | ) -> sqlx::Result<CommentRow> { |
| 277 | 1× | let row = sqlx::query_as( |
| 278 | 1× | "INSERT INTO proposal_comments (proposal_id, author_id, author, body, path, anchor_change, anchor_index, created_ms, via) VALUES ($1, $2, $3, $4, '', '', 0, $5, $6) RETURNING *", |
| 279 | 1× | ) |
| 280 | 1× | .bind(proposal_id) |
| 281 | 1× | .bind(author_id) |
| 282 | 1× | .bind(author) |
| 283 | 1× | .bind(body) |
| 284 | 1× | .bind(now_ms()) |
| 285 | 1× | .bind(via) |
| 286 | 1× | .fetch_one(pool) |
| 287 | 1× | .await?; |
| 288 | 1× | touch(pool, proposal_id).await?; |
| 289 | 1× | Ok(row) |
| 290 | 1× | } |
| 291 | |
|
| 292 | 0× | pub async fn set_comment_resolved(pool: &Pool, id: i64, resolved: bool) -> sqlx::Result<()> { |
| 293 | 0× | sqlx::query("UPDATE proposal_comments SET resolved = $1 WHERE id = $2").bind(resolved).bind(id).execute(pool).await?; |
| 294 | 0× | Ok(()) |
| 295 | 0× | } |
| 296 | |
|
| 297 | 146× | pub async fn reviews(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<ReviewRow>> { |
| 298 | 146× | sqlx::query_as("SELECT * FROM proposal_reviews WHERE proposal_id = $1 ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await |
| 299 | 146× | } |
| 300 | |
|
| 301 | 5× | pub async fn add_review(pool: &Pool, proposal_id: i64, author_id: i64, author: &str, verdict: &str, body: &str) -> sqlx::Result<ReviewRow> { |
| 302 | 5× | add_review_via(pool, proposal_id, author_id, author, verdict, body, "").await |
| 303 | 5× | } |
| 304 | |
|
| 305 | |
|
| 306 | 6× | pub async fn add_review_via( |
| 307 | 6× | pool: &Pool, |
| 308 | 6× | proposal_id: i64, |
| 309 | 6× | author_id: i64, |
| 310 | 6× | author: &str, |
| 311 | 6× | verdict: &str, |
| 312 | 6× | body: &str, |
| 313 | 6× | via: &str, |
| 314 | 6× | ) -> sqlx::Result<ReviewRow> { |
| 315 | 6× | let row = sqlx::query_as( |
| 316 | 6× | "INSERT INTO proposal_reviews (proposal_id, author_id, author, verdict, body, created_ms, via) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *", |
| 317 | 6× | ) |
| 318 | 6× | .bind(proposal_id) |
| 319 | 6× | .bind(author_id) |
| 320 | 6× | .bind(author) |
| 321 | 6× | .bind(verdict) |
| 322 | 6× | .bind(body) |
| 323 | 6× | .bind(now_ms()) |
| 324 | 6× | .bind(via) |
| 325 | 6× | .fetch_one(pool) |
| 326 | 6× | .await?; |
| 327 | 6× | touch(pool, proposal_id).await?; |
| 328 | 6× | Ok(row) |
| 329 | 6× | } |
| 330 | |
|
| 331 | 77× | pub async fn events(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<EventRow>> { |
| 332 | 77× | sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM proposal_events WHERE proposal_id = $1 ORDER BY at_ms, id") |
| 333 | 77× | .bind(proposal_id) |
| 334 | 77× | .fetch_all(pool) |
| 335 | 77× | .await |
| 336 | 77× | } |
| 337 | |
|
| 338 | |
|
| 339 | |
|
| 340 | 1× | pub async fn referenced_by(pool: &Pool, proposal_id: i64, hash: &str) -> sqlx::Result<bool> { |
| 341 | 1× | let n: i64 = sqlx::query_scalar( |
| 342 | 1× | "SELECT COUNT(*) FROM proposal_events WHERE proposal_id = $1 AND event = 'referenced by change' AND (detail = $2 OR detail LIKE $3)", |
| 343 | 1× | ) |
| 344 | 1× | .bind(proposal_id) |
| 345 | 1× | .bind(hash) |
| 346 | 1× | .bind(format!("{hash} %")) |
| 347 | 1× | .fetch_one(pool) |
| 348 | 1× | .await?; |
| 349 | 1× | Ok(n > 0) |
| 350 | 1× | } |
| 351 | |
|
| 352 | 95× | pub async fn add_event(pool: &Pool, proposal_id: i64, actor: &str, event: &str, detail: &str) -> sqlx::Result<()> { |
| 353 | 95× | sqlx::query("INSERT INTO proposal_events (proposal_id, at_ms, actor, event, detail) VALUES ($1, $2, $3, $4, $5)") |
| 354 | 95× | .bind(proposal_id) |
| 355 | 95× | .bind(now_ms()) |
| 356 | 95× | .bind(actor) |
| 357 | 95× | .bind(event) |
| 358 | 95× | .bind(detail) |
| 359 | 95× | .execute(pool) |
| 360 | 95× | .await?; |
| 361 | 95× | Ok(()) |
| 362 | 95× | } |
| 363 | |
|
| 364 | |
|
| 365 | | #[derive(Clone, Copy, Debug, Default, FromRow)] |
| 366 | | pub struct Requirements { |
| 367 | | pub required_approvals: i64, |
| 368 | | pub require_owners: bool, |
| 369 | | pub allow_conflicts: bool, |
| 370 | | } |
| 371 | |
|
| 372 | 281× | pub async fn requirements(pool: &Pool, repo_id: i64) -> sqlx::Result<Requirements> { |
| 373 | 281× | sqlx::query_as("SELECT required_approvals, require_owners, allow_conflicts FROM repositories WHERE id = $1") |
| 374 | 281× | .bind(repo_id) |
| 375 | 281× | .fetch_one(pool) |
| 376 | 281× | .await |
| 377 | 281× | } |
| 378 | |
|
| 379 | 4× | pub async fn set_requirements(pool: &Pool, repo_id: i64, r: Requirements) -> sqlx::Result<()> { |
| 380 | 4× | sqlx::query("UPDATE repositories SET required_approvals = $1, require_owners = $2, allow_conflicts = $3 WHERE id = $4") |
| 381 | 4× | .bind(r.required_approvals) |
| 382 | 4× | .bind(r.require_owners) |
| 383 | 4× | .bind(r.allow_conflicts) |
| 384 | 4× | .bind(repo_id) |
| 385 | 4× | .execute(pool) |
| 386 | 4× | .await?; |
| 387 | 4× | Ok(()) |
| 388 | 4× | } |
| 389 | |
|
| 390 | |
|
| 391 | |
|
| 392 | |
|
| 393 | 3× | pub async fn request_review(pool: &Pool, proposal_id: i64, account_id: i64, by: &str) -> sqlx::Result<bool> { |
| 394 | 3× | let r = sqlx::query( |
| 395 | 3× | "INSERT INTO proposal_review_requests (proposal_id, account_id, requested_by, requested_ms) VALUES ($1, $2, $3, $4) ON CONFLICT DO NOTHING", |
| 396 | 3× | ) |
| 397 | 3× | .bind(proposal_id) |
| 398 | 3× | .bind(account_id) |
| 399 | 3× | .bind(by) |
| 400 | 3× | .bind(now_ms()) |
| 401 | 3× | .execute(pool) |
| 402 | 3× | .await?; |
| 403 | 3× | Ok(r.rows_affected() == 1) |
| 404 | 3× | } |
| 405 | |
|
| 406 | |
|
| 407 | 6× | pub async fn unrequest_review(pool: &Pool, proposal_id: i64, account_id: i64) -> sqlx::Result<bool> { |
| 408 | 6× | let r = sqlx::query("DELETE FROM proposal_review_requests WHERE proposal_id = $1 AND account_id = $2") |
| 409 | 6× | .bind(proposal_id) |
| 410 | 6× | .bind(account_id) |
| 411 | 6× | .execute(pool) |
| 412 | 6× | .await?; |
| 413 | 6× | Ok(r.rows_affected() == 1) |
| 414 | 6× | } |
| 415 | |
|
| 416 | |
|
| 417 | |
|
| 418 | 119× | pub async fn review_requests(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<String>> { |
| 419 | 119× | sqlx::query_scalar( |
| 420 | 119× | "SELECT a.username FROM proposal_review_requests r JOIN accounts a ON a.id = r.account_id \ |
| 421 | 119× | WHERE r.proposal_id = $1 ORDER BY r.requested_ms, a.username", |
| 422 | 119× | ) |
| 423 | 119× | .bind(proposal_id) |
| 424 | 119× | .fetch_all(pool) |
| 425 | 119× | .await |
| 426 | 119× | } |
| 427 | |
|
| 428 | |
|
| 429 | 6× | pub async fn set_auto_land(pool: &Pool, id: i64, by: &str) -> sqlx::Result<()> { |
| 430 | 6× | sqlx::query("UPDATE proposals SET auto_land_by = $1, auto_land_ms = $2 WHERE id = $3") |
| 431 | 6× | .bind(by) |
| 432 | 6× | .bind((!by.is_empty()).then(now_ms)) |
| 433 | 6× | .bind(id) |
| 434 | 6× | .execute(pool) |
| 435 | 6× | .await?; |
| 436 | 6× | Ok(()) |
| 437 | 6× | } |
| 438 | |
|
| 439 | |
|
| 440 | |
|
| 441 | 705× | pub async fn auto_landing(pool: &Pool, repo_id: i64, state: &str) -> sqlx::Result<Vec<ProposalRow>> { |
| 442 | 705× | sqlx::query_as( |
| 443 | 705× | "SELECT * FROM proposals WHERE repo_id = $1 AND state = 'open' AND auto_land_by != '' AND projected_state = $2 ORDER BY number", |
| 444 | 705× | ) |
| 445 | 705× | .bind(repo_id) |
| 446 | 705× | .bind(state) |
| 447 | 705× | .fetch_all(pool) |
| 448 | 705× | .await |
| 449 | 705× | } |