OXYGENHUB

Postgres, step 1: every query numbers its placeholders $1, $2, ... 5d90e0e045e2

The hub's 507 SQL strings now use Postgres's placeholder form, which
SQLite (through sqlx) accepts too, so one text serves both databases
(oxygenhub#211). Converted by a script that leaves quoted SQL strings,
format braces and regexes alone; SQLite's numbered ?N became $N; the
three statements that build their placeholders (test case filters, backup
restore inserts and updates) and the inbox's assembled query number them
as they go. No behaviour change: 247 tests on SQLite.

Refs #211
Author
Andy McCall @andymccall <[email protected]>
via
agent Claude Code <[email protected]> · delegate for changes, until 2026-11-13 19:00Z · key ea5f78160088
When
· 2026-10-16 11:15 UTC
Signature
✔ signed by Claude Code
Hash
5d90e0e045e2765ec3216fdc770fe79882c0f8460dc4fe9cf573eadecec94b14
Depends on
0024ccb1b050 070a7b64b368 07b1a5bbf481 0b203bbf7cc3 0b20f1ec4007 0e04dfc20211 101740454617 10708ab8f54d 1553bfc9ae53 180dc43ad0e0 1870e4fa9808 18e381ea88c2 19bb31ff1f5c 1ac84c62ba53 1e8a6e8bb91b 1edac4b68c6d 1f78655a0466 20b8fe85888b 25ab8c73514a 2a0c09c09716 2a83f5303458 2fb70d019851 34beb1a7e376 36e5a9c15a84 3776b70d9b17 3d5c825aebba 3f4b0ea6eabf 3ff52534f69b 40dd3f86379a 417184c73f4e 42fa95540468 44a3d2ebaddd 4597750166e7 462f88d161ce 47e374020b59 4a4400c5812d 4ac01a732552 4d2e44d79b03 501d8b268dd4 554ccacfc123 5d54cf746e00 5dead1f4f26d 5ea4872f23e8 5ef6cf72cd19 60f11b71a740 617c2840446c 62d6e969f180 6a1780558353 6ca0b971ff07 736f6813543d 73ef9f767132 745d225de344 74be97962ed8 757195ad7a5d 7829b7c7bb59 7974adb0be66 7b9f4570daf5 7d2eb590051f 7edaff01d506 7f5f6c056d8f 83940d53664f 89e89836cae7 8ddebaa7e856 9008bf6c2b8c 90326c9601f9 92a797b20912 93501da2a162 941abdd6bb4c 9756c67d3aec 9aaa88b2d9c8 a0d4bbfbedd1 a160b98fb922 a36bab6b9ffc a3c8288b0141 a7e6e8e6094c b1e92a6233ee b644e1757711 b64e2537d702 b6b7e4cf6b24 bff9106edcba c8df33a6829f c987c86513df c98935537c57 cb71e18eb6f9 cc19697f8871 cc1cd197df1d d0fbea2eb46e d23464c0ac60 d3b048b27277 d7d182467382 d943160dafeb de505df5cf8d df5c69cbd047 df71c7892d27 e1ec9fbf538e e71583501b1d e71846aa07b9 e8e7158bbc73 e8fa53694f68 ea85398a51bc eb0c44a9ad37 ed89fae5180b eef677c48cd1 ef20fdc14e8b efd29a0033e8 f4d6ef418a96 f677d6270298 fd3db644df22 ff6569530173 ffb67a81188c
Depended on by
40a43921ef42 0bc95135954d 951aed50c2f5 1000b40a2629 8501fd5815b9 e4203e141e90 ab4df6aae179 439ecd8708a1 b349e15a3eab 367f34b8e590 166f6e9bf4f1 a9f1c0bf403a e97500b70f9a 05f1a85a2842 53a0fb2e4ac6 107eaba52c9d 55673c650986 f0f6c2d96b1c 3b1bc27fe805 97b18869acf5 ea14c4ec5c3f d1bf2d698777
In bookmarks
api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
In tags
v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
Edits
532 atoms, 0 tree operations, 32 files

Browse the tree at this change

modified crates/hub/src/api/mod.rs +1 −1
…
1400 unchanged lines
1401 1401
        Some(id) => db::issues::milestone(&app.db, id).await?.map(|m| m.title),
1402 1402
        None => None,
1403 1403
    };
1404 -
    let comments: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issue_comments WHERE issue_id = ?").bind(i.id).fetch_one(&app.db).await?;
1404 +
    let comments: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issue_comments WHERE issue_id = $1").bind(i.id).fetch_one(&app.db).await?;
1405 1405
    Ok(Issue {
1406 1406
        number: i.number,
1407 1407
        title: i.title.clone(),
…
1273 unchanged lines
modified crates/hub/src/apt.rs +1 −1
…
132 unchanged lines
133 133
        }
134 134
        let rows: Vec<(i64, String, String)> = sqlx::query_as(
135 135
            "SELECT a.id, a.name, a.hash FROM release_assets a JOIN releases r ON r.id = a.release_id \
136 -
             WHERE r.repo_id = ? AND a.name LIKE '%.deb' ORDER BY a.id",
136 +
             WHERE r.repo_id = $1 AND a.name LIKE '%.deb' ORDER BY a.id",
137 137
        )
138 138
        .bind(row.id)
139 139
        .fetch_all(&app.db)
…
280 unchanged lines
modified crates/hub/src/assistants.rs +26 −22
…
170 unchanged lines
171 171
// ------------------------------------------------------------- storage
172 172
173 173
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Assistant>> {
174 -
    sqlx::query_as("SELECT * FROM assistants WHERE scope = ? AND scope_id = ? ORDER BY name")
174 +
    sqlx::query_as("SELECT * FROM assistants WHERE scope = $1 AND scope_id = $2 ORDER BY name")
175 175
        .bind(scope.kind())
176 176
        .bind(scope.id())
177 177
        .fetch_all(pool)
…
1 unchanged lines
179 179
}
180 180
181 181
pub async fn get(pool: &Pool, id: i64) -> sqlx::Result<Option<Assistant>> {
182 -
    sqlx::query_as("SELECT * FROM assistants WHERE id = ?").bind(id).fetch_optional(pool).await
182 +
    sqlx::query_as("SELECT * FROM assistants WHERE id = $1").bind(id).fetch_optional(pool).await
183 183
}
184 184
185 185
/// Add an assistant; `None` when the level already has one of that name.
…
1 unchanged lines
187 187
    let now = now_ms();
188 188
    let r = sqlx::query_as(
189 189
        "INSERT INTO assistants (scope, scope_id, name, endpoint, api, model, headers, token, enabled, budget, created_ms, updated_ms) \
190 -
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT DO NOTHING RETURNING *",
190 +
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ON CONFLICT DO NOTHING RETURNING *",
191 191
    )
192 192
    .bind(scope.kind())
193 193
    .bind(scope.id())
…
17 unchanged lines
211 211
pub async fn update(pool: &Pool, id: i64, f: &Fields, token: Option<&[u8]>) -> sqlx::Result<bool> {
212 212
    let r = match token {
213 213
        Some(t) => sqlx::query(
214 -
            "UPDATE assistants SET name = ?, endpoint = ?, api = ?, model = ?, headers = ?, enabled = ?, budget = ?, token = ?, updated_ms = ? \
215 -
             WHERE id = ? AND NOT EXISTS (SELECT 1 FROM assistants o WHERE o.scope = assistants.scope AND o.scope_id = assistants.scope_id AND o.name = ? AND o.id <> ?)",
214 +
            "UPDATE assistants SET name = $1, endpoint = $2, api = $3, model = $4, headers = $5, enabled = $6, budget = $7, token = $8, updated_ms = $9 \
215 +
             WHERE id = $10 AND NOT EXISTS (SELECT 1 FROM assistants o WHERE o.scope = assistants.scope AND o.scope_id = assistants.scope_id AND o.name = $11 AND o.id <> $12)",
216 216
        )
217 217
        .bind(&f.name)
218 218
        .bind(&f.endpoint)
…
10 unchanged lines
229 229
        .execute(pool)
230 230
        .await?,
231 231
        None => sqlx::query(
232 -
            "UPDATE assistants SET name = ?, endpoint = ?, api = ?, model = ?, headers = ?, enabled = ?, budget = ?, updated_ms = ? \
233 -
             WHERE id = ? AND NOT EXISTS (SELECT 1 FROM assistants o WHERE o.scope = assistants.scope AND o.scope_id = assistants.scope_id AND o.name = ? AND o.id <> ?)",
232 +
            "UPDATE assistants SET name = $1, endpoint = $2, api = $3, model = $4, headers = $5, enabled = $6, budget = $7, updated_ms = $8 \
233 +
             WHERE id = $9 AND NOT EXISTS (SELECT 1 FROM assistants o WHERE o.scope = assistants.scope AND o.scope_id = assistants.scope_id AND o.name = $10 AND o.id <> $11)",
234 234
        )
235 235
        .bind(&f.name)
236 236
        .bind(&f.endpoint)
…
23 unchanged lines
260 260
    match scope {
261 261
        Scope::Site => {
262 262
            for table in ["proposal_reviews", "proposal_comments"] {
263 -
                sqlx::query(&format!("UPDATE {table} SET via = ? WHERE via = ?")).bind(new).bind(old).execute(pool).await?;
263 +
                sqlx::query(&format!("UPDATE {table} SET via = $1 WHERE via = $2")).bind(new).bind(old).execute(pool).await?;
264 264
            }
265 -
            sqlx::query("UPDATE proposal_events SET event = replace(event, ?, ?) WHERE event LIKE ?")
265 +
            sqlx::query("UPDATE proposal_events SET event = replace(event, $1, $2) WHERE event LIKE $3")
266 266
                .bind(old)
267 267
                .bind(new)
268 268
                .bind(&like)
…
1 unchanged lines
270 270
                .await?;
271 271
        }
272 272
        Scope::Org(_) => {
273 -
            let in_org = "proposal_id IN (SELECT p.id FROM proposals p JOIN repositories r ON r.id = p.repo_id WHERE r.owner = ?)";
273 +
            let in_org = "proposal_id IN (SELECT p.id FROM proposals p JOIN repositories r ON r.id = p.repo_id WHERE r.owner = $1)";
274 274
            for table in ["proposal_reviews", "proposal_comments"] {
275 -
                sqlx::query(&format!("UPDATE {table} SET via = ? WHERE via = ? AND {in_org}"))
275 +
                sqlx::query(&format!("UPDATE {table} SET via = $1 WHERE via = $2 AND {in_org}"))
276 276
                    .bind(new)
277 277
                    .bind(old)
278 278
                    .bind(owner_hint)
279 279
                    .execute(pool)
280 280
                    .await?;
281 281
            }
282 -
            sqlx::query(&format!("UPDATE proposal_events SET event = replace(event, ?, ?) WHERE event LIKE ? AND {in_org}"))
282 +
            sqlx::query(&format!("UPDATE proposal_events SET event = replace(event, $1, $2) WHERE event LIKE $3 AND {in_org}"))
283 283
                .bind(old)
284 284
                .bind(new)
285 285
                .bind(&like)
…
2 unchanged lines
288 288
                .await?;
289 289
        }
290 290
        Scope::Repo(id) => {
291 -
            let in_repo = "proposal_id IN (SELECT id FROM proposals WHERE repo_id = ?)";
291 +
            let in_repo = "proposal_id IN (SELECT id FROM proposals WHERE repo_id = $1)";
292 292
            for table in ["proposal_reviews", "proposal_comments"] {
293 -
                sqlx::query(&format!("UPDATE {table} SET via = ? WHERE via = ? AND {in_repo}"))
293 +
                sqlx::query(&format!("UPDATE {table} SET via = $1 WHERE via = $2 AND {in_repo}"))
294 294
                    .bind(new)
295 295
                    .bind(old)
296 296
                    .bind(id)
297 297
                    .execute(pool)
298 298
                    .await?;
299 299
            }
300 -
            sqlx::query(&format!("UPDATE proposal_events SET event = replace(event, ?, ?) WHERE event LIKE ? AND {in_repo}"))
300 +
            sqlx::query(&format!("UPDATE proposal_events SET event = replace(event, $1, $2) WHERE event LIKE $3 AND {in_repo}"))
301 301
                .bind(old)
302 302
                .bind(new)
303 303
                .bind(&like)
…
6 unchanged lines
310 310
}
311 311
312 312
pub async fn set_enabled(pool: &Pool, id: i64, enabled: bool) -> sqlx::Result<()> {
313 -
    sqlx::query("UPDATE assistants SET enabled = ?, updated_ms = ? WHERE id = ?")
313 +
    sqlx::query("UPDATE assistants SET enabled = $1, updated_ms = $2 WHERE id = $3")
314 314
        .bind(enabled)
315 315
        .bind(now_ms())
316 316
        .bind(id)
…
3 unchanged lines
320 320
}
321 321
322 322
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
323 -
    sqlx::query("DELETE FROM assistants WHERE id = ?").bind(id).execute(pool).await?;
323 +
    sqlx::query("DELETE FROM assistants WHERE id = $1").bind(id).execute(pool).await?;
324 324
    Ok(())
325 325
}
326 326
327 327
/// Forget a level's assistants and prompt, when the organisation or
328 328
/// repository goes.
329 329
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> {
330 -
    sqlx::query("DELETE FROM assistants WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
331 -
    sqlx::query("DELETE FROM assistant_prompts WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
330 +
    sqlx::query("DELETE FROM assistants WHERE scope = $1 AND scope_id = $2").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
331 +
    sqlx::query("DELETE FROM assistant_prompts WHERE scope = $1 AND scope_id = $2")
332 +
        .bind(scope.kind())
333 +
        .bind(scope.id())
334 +
        .execute(pool)
335 +
        .await?;
332 336
    Ok(())
333 337
}
334 338
335 339
/// The prompt a level adds, empty when none.
336 340
pub async fn prompt(pool: &Pool, scope: Scope) -> sqlx::Result<String> {
337 -
    Ok(sqlx::query_scalar("SELECT prompt FROM assistant_prompts WHERE scope = ? AND scope_id = ?")
341 +
    Ok(sqlx::query_scalar("SELECT prompt FROM assistant_prompts WHERE scope = $1 AND scope_id = $2")
338 342
        .bind(scope.kind())
339 343
        .bind(scope.id())
340 344
        .fetch_optional(pool)
…
3 unchanged lines
344 348
345 349
pub async fn set_prompt(pool: &Pool, scope: Scope, text: &str) -> sqlx::Result<()> {
346 350
    if text.trim().is_empty() {
347 -
        sqlx::query("DELETE FROM assistant_prompts WHERE scope = ? AND scope_id = ?")
351 +
        sqlx::query("DELETE FROM assistant_prompts WHERE scope = $1 AND scope_id = $2")
348 352
            .bind(scope.kind())
349 353
            .bind(scope.id())
350 354
            .execute(pool)
351 355
            .await?;
352 356
    } else {
353 357
        sqlx::query(
354 -
            "INSERT INTO assistant_prompts (scope, scope_id, prompt, updated_ms) VALUES (?, ?, ?, ?) \
358 +
            "INSERT INTO assistant_prompts (scope, scope_id, prompt, updated_ms) VALUES ($1, $2, $3, $4) \
355 359
             ON CONFLICT (scope, scope_id) DO UPDATE SET prompt = excluded.prompt, updated_ms = excluded.updated_ms",
356 360
        )
357 361
        .bind(scope.kind())
…
685 unchanged lines
modified crates/hub/src/backups.rs +43 −39
…
86 unchanged lines
87 87
}
88 88
89 89
pub async fn setting(pool: &Pool, owner: &str) -> sqlx::Result<Option<Setting>> {
90 -
    sqlx::query_as("SELECT * FROM backup_settings WHERE owner = ?").bind(owner).fetch_optional(pool).await
90 +
    sqlx::query_as("SELECT * FROM backup_settings WHERE owner = $1").bind(owner).fetch_optional(pool).await
91 91
}
92 92
93 93
pub async fn save(pool: &Pool, s: &Setting) -> sqlx::Result<()> {
94 94
    sqlx::query(
95 95
        "INSERT INTO backup_settings (owner, kind, every, destination, target, prefix, provider, keep_days, updated_ms, region) \
96 -
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(owner) DO UPDATE SET kind = excluded.kind, every = excluded.every, \
96 +
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) ON CONFLICT(owner) DO UPDATE SET kind = excluded.kind, every = excluded.every, \
97 97
         destination = excluded.destination, target = excluded.target, prefix = excluded.prefix, provider = excluded.provider, \
98 98
         keep_days = excluded.keep_days, updated_ms = excluded.updated_ms, region = excluded.region",
99 99
    )
…
13 unchanged lines
113 113
}
114 114
115 115
pub async fn runs(pool: &Pool, owner: &str, limit: i64) -> sqlx::Result<Vec<Run>> {
116 -
    sqlx::query_as("SELECT * FROM backup_runs WHERE owner = ? ORDER BY id DESC LIMIT ?").bind(owner).bind(limit).fetch_all(pool).await
116 +
    sqlx::query_as("SELECT * FROM backup_runs WHERE owner = $1 ORDER BY id DESC LIMIT $2").bind(owner).bind(limit).fetch_all(pool).await
117 117
}
118 118
119 119
/// What a repository may choose about its backup (§5): with its owner's,
…
64 unchanged lines
184 184
    for s in settings {
185 185
        let last = runs(pool, &s.owner, 1).await?.into_iter().next();
186 186
        let failed_day: i64 =
187 -
            sqlx::query_scalar("SELECT COUNT(*) FROM backup_runs WHERE owner = ? AND status = 'failed' AND started_ms >= ?")
187 +
            sqlx::query_scalar("SELECT COUNT(*) FROM backup_runs WHERE owner = $1 AND status = 'failed' AND started_ms >= $2")
188 188
                .bind(&s.owner)
189 189
                .bind(day_ago)
190 190
                .fetch_one(pool)
…
860 unchanged lines
1051 1051
    }
1052 1052
    let mut out = serde_json::Map::new();
1053 1053
    for (kind, sql) in [
1054 -
        ("accounts", "SELECT id, username FROM accounts WHERE id = ?"),
1055 -
        ("organisations", "SELECT id, name FROM organisations WHERE id = ?"),
1056 -
        ("teams", "SELECT t.id, o.name || '/' || t.name FROM teams t JOIN organisations o ON o.id = t.org_id WHERE t.id = ?"),
1054 +
        ("accounts", "SELECT id, username FROM accounts WHERE id = $1"),
1055 +
        ("organisations", "SELECT id, name FROM organisations WHERE id = $1"),
1056 +
        ("teams", "SELECT t.id, o.name || '/' || t.name FROM teams t JOIN organisations o ON o.id = t.org_id WHERE t.id = $1"),
1057 1057
    ] {
1058 1058
        let mut m = serde_json::Map::new();
1059 1059
        for id in ids.get(kind).into_iter().flatten() {
…
9 unchanged lines
1069 1069
/// The id `name` has on this hub as one of `_names`' kinds.
1070 1070
async fn id_by_name(pool: &Pool, kind: &str, name: &str) -> Result<Option<i64>> {
1071 1071
    Ok(match kind {
1072 -
        "accounts" => sqlx::query_scalar("SELECT id FROM accounts WHERE username = ?").bind(name).fetch_optional(pool).await?,
1073 -
        "organisations" => sqlx::query_scalar("SELECT id FROM organisations WHERE name = ?").bind(name).fetch_optional(pool).await?,
1072 +
        "accounts" => sqlx::query_scalar("SELECT id FROM accounts WHERE username = $1").bind(name).fetch_optional(pool).await?,
1073 +
        "organisations" => sqlx::query_scalar("SELECT id FROM organisations WHERE name = $1").bind(name).fetch_optional(pool).await?,
1074 1074
        "teams" => match name.split_once('/') {
1075 1075
            Some((org, team)) => {
1076 -
                sqlx::query_scalar("SELECT t.id FROM teams t JOIN organisations o ON o.id = t.org_id WHERE o.name = ? AND t.name = ?")
1076 +
                sqlx::query_scalar("SELECT t.id FROM teams t JOIN organisations o ON o.id = t.org_id WHERE o.name = $1 AND t.name = $2")
1077 1077
                    .bind(org)
1078 1078
                    .bind(team)
1079 1079
                    .fetch_optional(pool)
…
147 unchanged lines
1227 1227
            let sql = format!(
1228 1228
                "{verb} INTO \"{table}\" ({}) VALUES ({})",
1229 1229
                names.iter().map(|n| format!("\"{n}\"")).collect::<Vec<_>>().join(", "),
1230 -
                vec!["?"; names.len()].join(", ")
1230 +
                (1..=names.len()).map(|i| format!("${i}")).collect::<Vec<_>>().join(", ")
1231 1231
            );
1232 1232
            let mut q = sqlx::query(&sql);
1233 1233
            for (v, blob) in values {
…
12 unchanged lines
1246 1246
    // Search finds what came back.
1247 1247
    for (kind, table) in [("issue", "issues"), ("proposal", "proposals")] {
1248 1248
        let found: Vec<(i64, i64, String, String)> =
1249 -
            sqlx::query_as(&format!("SELECT id, number, title, body FROM {table} WHERE repo_id = ?")).bind(repo).fetch_all(pool).await?;
1249 +
            sqlx::query_as(&format!("SELECT id, number, title, body FROM {table} WHERE repo_id = $1")).bind(repo).fetch_all(pool).await?;
1250 1250
        for (id, number, title, body) in found {
1251 1251
            db::notify::index(pool, kind, repo, id, number, &title, &body).await?;
1252 1252
        }
1253 1253
    }
1254 1254
    // Webhook secrets were never written: they are entered again.
1255 1255
    let hooks: Vec<String> =
1256 -
        sqlx::query_scalar("SELECT url FROM hooks WHERE repo_id = ? AND secret = ''").bind(repo).fetch_all(pool).await?;
1256 +
        sqlx::query_scalar("SELECT url FROM hooks WHERE repo_id = $1 AND secret = ''").bind(repo).fetch_all(pool).await?;
1257 1257
    for url in hooks {
1258 1258
        notes.push(format!("webhook {url}: enter its secret again (Settings › Webhooks)"));
1259 1259
    }
…
44 unchanged lines
1304 1304
    let s = setting(&app.db, owner).await?.with_context(|| format!("{owner} has no backup set up"))?;
1305 1305
    let started = db::now_ms();
1306 1306
    let at = run_name(started);
1307 -
    let id: i64 =
1308 -
        sqlx::query_scalar("INSERT INTO backup_runs (owner, started_ms, status, at, trigger) VALUES (?, ?, 'running', ?, ?) RETURNING id")
1309 -
            .bind(owner)
1310 -
            .bind(started)
1311 -
            .bind(&at)
1312 -
            .bind(trigger)
1313 -
            .fetch_one(&app.db)
1314 -
            .await?;
1307 +
    let id: i64 = sqlx::query_scalar(
1308 +
        "INSERT INTO backup_runs (owner, started_ms, status, at, trigger) VALUES ($1, $2, 'running', $3, $4) RETURNING id",
1309 +
    )
1310 +
    .bind(owner)
1311 +
    .bind(started)
1312 +
    .bind(&at)
1313 +
    .bind(trigger)
1314 +
    .fetch_one(&app.db)
1315 +
    .await?;
1315 1316
    let outcome = run_into(app, &s, &at, trigger == "now").await;
1316 1317
    let (status, detail, sent) = match outcome {
1317 1318
        Ok((sent, problems)) if problems.is_empty() => ("ok", String::new(), sent),
1318 1319
        Ok((sent, problems)) => ("partial", problems.join("; "), sent),
1319 1320
        Err(e) => ("failed", format!("{e:#}"), (0, Sent::default())),
1320 1321
    };
1321 -
    sqlx::query("UPDATE backup_runs SET finished_ms = ?, status = ?, detail = ?, repos = ?, files = ?, sent = ?, bytes = ? WHERE id = ?")
1322 -
        .bind(db::now_ms())
1323 -
        .bind(status)
1324 -
        .bind(&detail)
1325 -
        .bind(sent.0)
1326 -
        .bind(sent.1.files)
1327 -
        .bind(sent.1.sent)
1328 -
        .bind(sent.1.bytes)
1329 -
        .bind(id)
1330 -
        .execute(&app.db)
1331 -
        .await?;
1332 -
    let r: Run = sqlx::query_as("SELECT * FROM backup_runs WHERE id = ?").bind(id).fetch_one(&app.db).await?;
1322 +
    sqlx::query(
1323 +
        "UPDATE backup_runs SET finished_ms = $1, status = $2, detail = $3, repos = $4, files = $5, sent = $6, bytes = $7 WHERE id = $8",
1324 +
    )
1325 +
    .bind(db::now_ms())
1326 +
    .bind(status)
1327 +
    .bind(&detail)
1328 +
    .bind(sent.0)
1329 +
    .bind(sent.1.files)
1330 +
    .bind(sent.1.sent)
1331 +
    .bind(sent.1.bytes)
1332 +
    .bind(id)
1333 +
    .execute(&app.db)
1334 +
    .await?;
1335 +
    let r: Run = sqlx::query_as("SELECT * FROM backup_runs WHERE id = $1").bind(id).fetch_one(&app.db).await?;
1333 1336
    if status != "ok" {
1334 1337
        notify_failure(app, &s, status, &detail).await;
1335 1338
    } else {
…
186 unchanged lines
1522 1525
1523 1526
/// The status of `owner`'s finished run before the newest.
1524 1527
async fn status_before(app: &Shared, owner: &str) -> Option<String> {
1525 -
    sqlx::query_scalar("SELECT status FROM backup_runs WHERE owner = ? AND finished_ms IS NOT NULL ORDER BY id DESC LIMIT 1 OFFSET 1")
1528 +
    sqlx::query_scalar("SELECT status FROM backup_runs WHERE owner = $1 AND finished_ms IS NOT NULL ORDER BY id DESC LIMIT 1 OFFSET 1")
1526 1529
        .bind(owner)
1527 1530
        .fetch_optional(&app.db)
1528 1531
        .await
…
40 unchanged lines
1569 1572
    let settings: Vec<Setting> = sqlx::query_as("SELECT * FROM backup_settings WHERE updated_ms > 0").fetch_all(&app.db).await?;
1570 1573
    let mut sent = 0;
1571 1574
    for s in settings {
1572 -
        let runs: Vec<Run> = sqlx::query_as("SELECT * FROM backup_runs WHERE owner = ? AND started_ms >= ? ORDER BY id")
1575 +
        let runs: Vec<Run> = sqlx::query_as("SELECT * FROM backup_runs WHERE owner = $1 AND started_ms >= $2 ORDER BY id")
1573 1576
            .bind(&s.owner)
1574 1577
            .bind(now_ms - 86_400_000)
1575 1578
            .fetch_all(&app.db)
…
23 unchanged lines
1599 1602
/// "running" by a restart is marked failed first.
1600 1603
pub async fn scheduler(app: Shared) {
1601 1604
    let _ = sqlx::query(
1602 -
        "UPDATE backup_runs SET status = 'failed', detail = 'the hub restarted during the run', finished_ms = ? WHERE status = 'running'",
1605 +
        "UPDATE backup_runs SET status = 'failed', detail = 'the hub restarted during the run', finished_ms = $1 WHERE status = 'running'",
1603 1606
    )
1604 1607
    .bind(db::now_ms())
1605 1608
    .execute(&app.db)
…
24 unchanged lines
1630 1633
        let theirs: Vec<db::repos::RepoRow> = repos.iter().filter(|r| r.owner == s.owner).cloned().collect();
1631 1634
        let Some(period) = period_with(&s, &theirs) else { continue };
1632 1635
        let last: Option<i64> =
1633 -
            sqlx::query_scalar("SELECT MAX(started_ms) FROM backup_runs WHERE owner = ?").bind(&s.owner).fetch_one(pool).await?;
1636 +
            sqlx::query_scalar("SELECT MAX(started_ms) FROM backup_runs WHERE owner = $1").bind(&s.owner).fetch_one(pool).await?;
1634 1637
        if last.is_none_or(|l| now - l >= period * 1000) {
1635 1638
            out.push(s);
1636 1639
        }
…
72 unchanged lines
1709 1712
            .collect();
1710 1713
        if !set.is_empty() {
1711 1714
            let sql = format!(
1712 -
                "UPDATE repositories SET {} WHERE id = ?",
1713 -
                set.iter().map(|(c, _)| format!("\"{}\" = ?", c.name)).collect::<Vec<_>>().join(", ")
1715 +
                "UPDATE repositories SET {} WHERE id = ${}",
1716 +
                set.iter().enumerate().map(|(i, (c, _))| format!("\"{}\" = ${}", c.name, i + 1)).collect::<Vec<_>>().join(", "),
1717 +
                set.len() + 1
1714 1718
            );
1715 1719
            let mut q = sqlx::query(&sql);
1716 1720
            for (c, v) in set {
…
90 unchanged lines
modified crates/hub/src/coverage.rs +12 −12
…
464 unchanged lines
465 465
    let sum = |f: &dyn Fn(&FileCoverage) -> u64| kept.iter().map(f).sum::<u64>() as i64;
466 466
    let mut tx = crate::db::begin_write(&app.db).await?;
467 467
    if let Some(job) = o.job_id {
468 -
        sqlx::query("DELETE FROM coverage_reports WHERE job_id = ?").bind(job).execute(&mut *tx).await?;
468 +
        sqlx::query("DELETE FROM coverage_reports WHERE job_id = $1").bind(job).execute(&mut *tx).await?;
469 469
    }
470 470
    let id: i64 = sqlx::query_scalar(
471 471
        "INSERT INTO coverage_reports (repo_id, run_id, job_id, job, run_number, bookmark, state, heads, blob, format, \
472 472
         lines_found, lines_hit, functions_found, functions_hit, branches_found, branches_hit, hits, files, dropped, created_ms, uploader) \
473 -
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
473 +
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21) RETURNING id",
474 474
    )
475 475
    .bind(o.repo_id)
476 476
    .bind(o.run_id)
…
21 unchanged lines
498 498
    for f in &kept {
499 499
        sqlx::query(
500 500
            "INSERT INTO coverage_files (report_id, path, lines_found, lines_hit, functions_found, functions_hit, branches_found, branches_hit) \
501 -
             VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
501 +
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
502 502
        )
503 503
        .bind(id)
504 504
        .bind(&f.path)
…
12 unchanged lines
517 517
}
518 518
519 519
pub async fn report(pool: &Pool, repo_id: i64, id: i64) -> sqlx::Result<Option<Report>> {
520 -
    sqlx::query_as("SELECT * FROM coverage_reports WHERE id = ? AND repo_id = ?").bind(id).bind(repo_id).fetch_optional(pool).await
520 +
    sqlx::query_as("SELECT * FROM coverage_reports WHERE id = $1 AND repo_id = $2").bind(id).bind(repo_id).fetch_optional(pool).await
521 521
}
522 522
523 523
/// The newest reports on a bookmark, newest first.
524 524
pub async fn on_bookmark(pool: &Pool, repo_id: i64, bookmark: &str, limit: i64) -> sqlx::Result<Vec<Report>> {
525 -
    sqlx::query_as("SELECT * FROM coverage_reports WHERE repo_id = ? AND bookmark = ? ORDER BY created_ms DESC, id DESC LIMIT ?")
525 +
    sqlx::query_as("SELECT * FROM coverage_reports WHERE repo_id = $1 AND bookmark = $2 ORDER BY created_ms DESC, id DESC LIMIT $3")
526 526
        .bind(repo_id)
527 527
        .bind(bookmark)
528 528
        .bind(limit)
…
3 unchanged lines
532 532
533 533
/// When the repository's first report on any bookmark was kept.
534 534
pub async fn first_ms(pool: &Pool, repo_id: i64) -> sqlx::Result<Option<i64>> {
535 -
    sqlx::query_scalar("SELECT MIN(created_ms) FROM coverage_reports WHERE repo_id = ?").bind(repo_id).fetch_one(pool).await
535 +
    sqlx::query_scalar("SELECT MIN(created_ms) FROM coverage_reports WHERE repo_id = $1").bind(repo_id).fetch_one(pool).await
536 536
}
537 537
538 538
/// The bookmarks with reports, the most recent first.
539 539
pub async fn bookmarks(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
540 -
    sqlx::query_scalar("SELECT bookmark FROM coverage_reports WHERE repo_id = ? GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
540 +
    sqlx::query_scalar("SELECT bookmark FROM coverage_reports WHERE repo_id = $1 GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
541 541
        .bind(repo_id)
542 542
        .fetch_all(pool)
543 543
        .await
…
10 unchanged lines
554 554
555 555
pub async fn rules(pool: &Pool, repo_id: i64) -> sqlx::Result<Rules> {
556 556
    let row: Option<(Option<f64>, Option<f64>)> =
557 -
        sqlx::query_as("SELECT min, max_drop FROM coverage_settings WHERE repo_id = ?").bind(repo_id).fetch_optional(pool).await?;
557 +
        sqlx::query_as("SELECT min, max_drop FROM coverage_settings WHERE repo_id = $1").bind(repo_id).fetch_optional(pool).await?;
558 558
    Ok(row.map(|(min, max_drop)| Rules { min, max_drop }).unwrap_or_default())
559 559
}
560 560
561 561
pub async fn set_rules(pool: &Pool, repo_id: i64, r: Rules) -> sqlx::Result<()> {
562 562
    sqlx::query(
563 -
        "INSERT INTO coverage_settings (repo_id, min, max_drop) VALUES (?, ?, ?) \
563 +
        "INSERT INTO coverage_settings (repo_id, min, max_drop) VALUES ($1, $2, $3) \
564 564
         ON CONFLICT (repo_id) DO UPDATE SET min = excluded.min, max_drop = excluded.max_drop",
565 565
    )
566 566
    .bind(repo_id)
…
8 unchanged lines
575 575
/// before it there; on any other, the default bookmark's latest before it.
576 576
pub async fn baseline(pool: &Pool, r: &Report, default_bookmark: &str) -> sqlx::Result<Option<Report>> {
577 577
    sqlx::query_as(
578 -
        "SELECT * FROM coverage_reports WHERE repo_id = ? AND bookmark = ? AND id <> ? AND (created_ms < ? OR (created_ms = ? AND id < ?)) \
578 +
        "SELECT * FROM coverage_reports WHERE repo_id = $1 AND bookmark = $2 AND id <> $3 AND (created_ms < $4 OR (created_ms = $5 AND id < $6)) \
579 579
         ORDER BY created_ms DESC, id DESC LIMIT 1",
580 580
    )
581 581
    .bind(r.repo_id)
…
27 unchanged lines
609 609
}
610 610
611 611
pub async fn for_run(pool: &Pool, run_id: i64) -> sqlx::Result<Vec<Report>> {
612 -
    sqlx::query_as("SELECT * FROM coverage_reports WHERE run_id = ? ORDER BY job").bind(run_id).fetch_all(pool).await
612 +
    sqlx::query_as("SELECT * FROM coverage_reports WHERE run_id = $1 ORDER BY job").bind(run_id).fetch_all(pool).await
613 613
}
614 614
615 615
pub async fn files(pool: &Pool, report_id: i64) -> sqlx::Result<Vec<FileRow>> {
616 -
    sqlx::query_as("SELECT * FROM coverage_files WHERE report_id = ? ORDER BY path").bind(report_id).fetch_all(pool).await
616 +
    sqlx::query_as("SELECT * FROM coverage_files WHERE report_id = $1 ORDER BY path").bind(report_id).fetch_all(pool).await
617 617
}
618 618
619 619
#[cfg(test)]
…
122 unchanged lines
modified crates/hub/src/db/accounts.rs +94 −91
…
142 unchanged lines
143 143
// ---------------------------------------------------------------- accounts
144 144
145 145
pub async fn account_by_name(pool: &Pool, username: &str) -> sqlx::Result<Option<Account>> {
146 -
    sqlx::query_as("SELECT * FROM accounts WHERE username = ?").bind(username).fetch_optional(pool).await
146 +
    sqlx::query_as("SELECT * FROM accounts WHERE username = $1").bind(username).fetch_optional(pool).await
147 147
}
148 148
149 149
pub async fn account_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<Account>> {
150 -
    sqlx::query_as("SELECT * FROM accounts WHERE id = ?").bind(id).fetch_optional(pool).await
150 +
    sqlx::query_as("SELECT * FROM accounts WHERE id = $1").bind(id).fetch_optional(pool).await
151 151
}
152 152
153 153
pub async fn accounts(pool: &Pool) -> sqlx::Result<Vec<Account>> {
…
13 unchanged lines
167 167
    is_admin: bool,
168 168
) -> sqlx::Result<Account> {
169 169
    sqlx::query_as(
170 -
        "INSERT INTO accounts (username, email, password_hash, session_salt, is_admin, created_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING *",
170 +
        "INSERT INTO accounts (username, email, password_hash, session_salt, is_admin, created_ms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *",
171 171
    )
172 172
    .bind(username)
173 173
    .bind(email)
…
8 unchanged lines
182 182
pub async fn set_profile(pool: &Pool, id: i64, display_name: &str, email: &str) -> sqlx::Result<()> {
183 183
    // A changed address is unverified until its own link is used.
184 184
    sqlx::query(
185 -
        "UPDATE accounts SET display_name = ?, email_verified = CASE WHEN email = ? THEN email_verified ELSE 0 END, email = ? WHERE id = ?",
185 +
        "UPDATE accounts SET display_name = $1, email_verified = CASE WHEN email = $2 THEN email_verified ELSE 0 END, email = $3 WHERE id = $4",
186 186
    )
187 187
    .bind(display_name)
188 188
    .bind(email)
…
7 unchanged lines
196 196
/// The public profile fields.
197 197
/// The accounts elsewhere a profile shows (#105), already normalised.
198 198
pub async fn set_links(pool: &Pool, id: i64, bluesky: &str, mastodon: &str, github: &str) -> sqlx::Result<()> {
199 -
    sqlx::query("UPDATE accounts SET bluesky = ?, mastodon = ?, github = ? WHERE id = ?")
199 +
    sqlx::query("UPDATE accounts SET bluesky = $1, mastodon = $2, github = $3 WHERE id = $4")
200 200
        .bind(bluesky)
201 201
        .bind(mastodon)
202 202
        .bind(github)
…
4 unchanged lines
207 207
}
208 208
209 209
pub async fn set_bio(pool: &Pool, id: i64, bio: &str, website: &str, location: &str) -> sqlx::Result<()> {
210 -
    sqlx::query("UPDATE accounts SET bio = ?, website = ?, location = ? WHERE id = ?")
210 +
    sqlx::query("UPDATE accounts SET bio = $1, website = $2, location = $3 WHERE id = $4")
211 211
        .bind(bio)
212 212
        .bind(website)
213 213
        .bind(location)
…
4 unchanged lines
218 218
}
219 219
220 220
pub async fn set_password(pool: &Pool, id: i64, password_hash: &str, session_salt: &str) -> sqlx::Result<()> {
221 -
    sqlx::query("UPDATE accounts SET password_hash = ?, session_salt = ? WHERE id = ?")
221 +
    sqlx::query("UPDATE accounts SET password_hash = $1, session_salt = $2 WHERE id = $3")
222 222
        .bind(password_hash)
223 223
        .bind(session_salt)
224 224
        .bind(id)
…
3 unchanged lines
228 228
}
229 229
230 230
pub async fn set_session_salt(pool: &Pool, id: i64, session_salt: &str) -> sqlx::Result<()> {
231 -
    sqlx::query("UPDATE accounts SET session_salt = ? WHERE id = ?").bind(session_salt).bind(id).execute(pool).await?;
231 +
    sqlx::query("UPDATE accounts SET session_salt = $1 WHERE id = $2").bind(session_salt).bind(id).execute(pool).await?;
232 232
    Ok(())
233 233
}
234 234
235 235
pub async fn set_totp(pool: &Pool, id: i64, secret: Option<&str>, recovery_codes: &str) -> sqlx::Result<()> {
236 -
    sqlx::query("UPDATE accounts SET totp_secret = ?, recovery_codes = ? WHERE id = ?")
236 +
    sqlx::query("UPDATE accounts SET totp_secret = $1, recovery_codes = $2 WHERE id = $3")
237 237
        .bind(secret)
238 238
        .bind(recovery_codes)
239 239
        .bind(id)
…
3 unchanged lines
243 243
}
244 244
245 245
pub async fn set_admin(pool: &Pool, id: i64, is_admin: bool) -> sqlx::Result<()> {
246 -
    sqlx::query("UPDATE accounts SET is_admin = ? WHERE id = ?").bind(is_admin).bind(id).execute(pool).await?;
246 +
    sqlx::query("UPDATE accounts SET is_admin = $1 WHERE id = $2").bind(is_admin).bind(id).execute(pool).await?;
247 247
    Ok(())
248 248
}
249 249
250 250
pub async fn set_banned(pool: &Pool, id: i64, banned: bool) -> sqlx::Result<()> {
251 -
    sqlx::query("UPDATE accounts SET banned = ? WHERE id = ?").bind(banned).bind(id).execute(pool).await?;
251 +
    sqlx::query("UPDATE accounts SET banned = $1 WHERE id = $2").bind(banned).bind(id).execute(pool).await?;
252 252
    Ok(())
253 253
}
254 254
255 255
pub async fn set_disabled(pool: &Pool, id: i64, disabled: bool) -> sqlx::Result<()> {
256 -
    sqlx::query("UPDATE accounts SET disabled = ? WHERE id = ?").bind(disabled).bind(id).execute(pool).await?;
256 +
    sqlx::query("UPDATE accounts SET disabled = $1 WHERE id = $2").bind(disabled).bind(id).execute(pool).await?;
257 257
    Ok(())
258 258
}
259 259
…
3 unchanged lines
263 263
/// is retired so nobody can register it and pass for the account.
264 264
pub async fn delete_account(pool: &Pool, id: i64, username: &str) -> sqlx::Result<()> {
265 265
    let mut tx = super::begin_write(pool).await?;
266 -
    sqlx::query("INSERT OR IGNORE INTO retired_usernames (username, retired_ms) VALUES (?, ?)")
266 +
    sqlx::query("INSERT OR IGNORE INTO retired_usernames (username, retired_ms) VALUES ($1, $2)")
267 267
        .bind(username)
268 268
        .bind(now_ms())
269 269
        .execute(&mut *tx)
270 270
        .await?;
271 -
    sqlx::query("DELETE FROM accounts WHERE id = ?").bind(id).execute(&mut *tx).await?;
271 +
    sqlx::query("DELETE FROM accounts WHERE id = $1").bind(id).execute(&mut *tx).await?;
272 272
    tx.commit().await
273 273
}
274 274
…
2 unchanged lines
277 277
pub async fn orgs_owned_only_by(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<String>> {
278 278
    sqlx::query_scalar(
279 279
        "SELECT o.name FROM organisations o JOIN org_members m ON m.org_id = o.id \
280 -
         WHERE m.account_id = ? AND m.role = 'owner' \
280 +
         WHERE m.account_id = $1 AND m.role = 'owner' \
281 281
           AND (SELECT COUNT(*) FROM org_members x WHERE x.org_id = o.id AND x.role = 'owner') = 1 \
282 282
         ORDER BY o.name",
283 283
    )
…
4 unchanged lines
288 288
289 289
/// Whether `name` belonged to a deleted account.
290 290
pub async fn username_retired(pool: &Pool, name: &str) -> sqlx::Result<bool> {
291 -
    Ok(sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM retired_usernames WHERE username = ?").bind(name).fetch_one(pool).await? > 0)
291 +
    Ok(sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM retired_usernames WHERE username = $1").bind(name).fetch_one(pool).await? > 0)
292 292
}
293 293
294 294
pub async fn touch_seen(pool: &Pool, id: i64) -> sqlx::Result<()> {
295 -
    sqlx::query("UPDATE accounts SET last_seen_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
295 +
    sqlx::query("UPDATE accounts SET last_seen_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
296 296
    Ok(())
297 297
}
298 298
299 299
// ----------------------------------------------------------- organisations
300 300
301 301
pub async fn org_by_name(pool: &Pool, name: &str) -> sqlx::Result<Option<Organisation>> {
302 -
    sqlx::query_as("SELECT * FROM organisations WHERE name = ?").bind(name).fetch_optional(pool).await
302 +
    sqlx::query_as("SELECT * FROM organisations WHERE name = $1").bind(name).fetch_optional(pool).await
303 303
}
304 304
305 305
pub async fn orgs(pool: &Pool) -> sqlx::Result<Vec<Organisation>> {
…
10 unchanged lines
316 316
317 317
pub async fn orgs_of(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<(Organisation, String)>> {
318 318
    let rows: Vec<Membership> = sqlx::query_as(
319 -
        "SELECT o.*, m.role FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = ? ORDER BY o.name",
319 +
        "SELECT o.*, m.role FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = $1 ORDER BY o.name",
320 320
    )
321 321
    .bind(account_id)
322 322
    .fetch_all(pool)
…
3 unchanged lines
326 326
327 327
pub async fn create_org(pool: &Pool, name: &str, display_name: &str, owner: i64) -> sqlx::Result<Organisation> {
328 328
    let mut tx = super::begin_write(pool).await?;
329 -
    let org: Organisation = sqlx::query_as("INSERT INTO organisations (name, display_name, created_ms) VALUES (?, ?, ?) RETURNING *")
329 +
    let org: Organisation = sqlx::query_as("INSERT INTO organisations (name, display_name, created_ms) VALUES ($1, $2, $3) RETURNING *")
330 330
        .bind(name)
331 331
        .bind(display_name)
332 332
        .bind(now_ms())
333 333
        .fetch_one(&mut *tx)
334 334
        .await?;
335 -
    sqlx::query("INSERT INTO org_members (org_id, account_id, role) VALUES (?, ?, 'owner')")
335 +
    sqlx::query("INSERT INTO org_members (org_id, account_id, role) VALUES ($1, $2, 'owner')")
336 336
        .bind(org.id)
337 337
        .bind(owner)
338 338
        .execute(&mut *tx)
…
3 unchanged lines
342 342
}
343 343
344 344
pub async fn set_org(pool: &Pool, id: i64, display_name: &str, description: &str, default_role: &str) -> sqlx::Result<()> {
345 -
    sqlx::query("UPDATE organisations SET display_name = ?, description = ?, default_role = ? WHERE id = ?")
345 +
    sqlx::query("UPDATE organisations SET display_name = $1, description = $2, default_role = $3 WHERE id = $4")
346 346
        .bind(display_name)
347 347
        .bind(description)
348 348
        .bind(default_role)
…
6 unchanged lines
355 355
/// `(account, role)` for every member.
356 356
pub async fn org_members(pool: &Pool, org_id: i64) -> sqlx::Result<Vec<(Account, String)>> {
357 357
    let rows: Vec<(i64, String)> =
358 -
        sqlx::query_as("SELECT account_id, role FROM org_members WHERE org_id = ?").bind(org_id).fetch_all(pool).await?;
358 +
        sqlx::query_as("SELECT account_id, role FROM org_members WHERE org_id = $1").bind(org_id).fetch_all(pool).await?;
359 359
    let mut out = Vec::new();
360 360
    for (id, role) in rows {
361 361
        if let Some(a) = account_by_id(pool, id).await? {
…
5 unchanged lines
367 367
}
368 368
369 369
pub async fn org_role(pool: &Pool, org_id: i64, account_id: i64) -> sqlx::Result<Option<String>> {
370 -
    sqlx::query_scalar("SELECT role FROM org_members WHERE org_id = ? AND account_id = ?")
370 +
    sqlx::query_scalar("SELECT role FROM org_members WHERE org_id = $1 AND account_id = $2")
371 371
        .bind(org_id)
372 372
        .bind(account_id)
373 373
        .fetch_optional(pool)
…
1 unchanged lines
375 375
}
376 376
377 377
pub async fn set_org_member(pool: &Pool, org_id: i64, account_id: i64, role: &str) -> sqlx::Result<()> {
378 -
    sqlx::query("INSERT INTO org_members (org_id, account_id, role) VALUES (?, ?, ?) ON CONFLICT (org_id, account_id) DO UPDATE SET role = excluded.role")
378 +
    sqlx::query("INSERT INTO org_members (org_id, account_id, role) VALUES ($1, $2, $3) ON CONFLICT (org_id, account_id) DO UPDATE SET role = excluded.role")
379 379
        .bind(org_id)
380 380
        .bind(account_id)
381 381
        .bind(role)
…
3 unchanged lines
385 385
}
386 386
387 387
pub async fn remove_org_member(pool: &Pool, org_id: i64, account_id: i64) -> sqlx::Result<()> {
388 -
    sqlx::query("DELETE FROM org_members WHERE org_id = ? AND account_id = ?").bind(org_id).bind(account_id).execute(pool).await?;
388 +
    sqlx::query("DELETE FROM org_members WHERE org_id = $1 AND account_id = $2").bind(org_id).bind(account_id).execute(pool).await?;
389 389
    Ok(())
390 390
}
391 391
392 392
pub async fn teams(pool: &Pool, org_id: i64) -> sqlx::Result<Vec<Team>> {
393 -
    sqlx::query_as("SELECT * FROM teams WHERE org_id = ? ORDER BY name").bind(org_id).fetch_all(pool).await
393 +
    sqlx::query_as("SELECT * FROM teams WHERE org_id = $1 ORDER BY name").bind(org_id).fetch_all(pool).await
394 394
}
395 395
396 396
pub async fn team_by_name(pool: &Pool, org_id: i64, name: &str) -> sqlx::Result<Option<Team>> {
397 -
    sqlx::query_as("SELECT * FROM teams WHERE org_id = ? AND name = ?").bind(org_id).bind(name).fetch_optional(pool).await
397 +
    sqlx::query_as("SELECT * FROM teams WHERE org_id = $1 AND name = $2").bind(org_id).bind(name).fetch_optional(pool).await
398 398
}
399 399
400 400
pub async fn create_team(pool: &Pool, org_id: i64, name: &str) -> sqlx::Result<Team> {
401 -
    sqlx::query_as("INSERT INTO teams (org_id, name) VALUES (?, ?) RETURNING *").bind(org_id).bind(name).fetch_one(pool).await
401 +
    sqlx::query_as("INSERT INTO teams (org_id, name) VALUES ($1, $2) RETURNING *").bind(org_id).bind(name).fetch_one(pool).await
402 402
}
403 403
404 404
pub async fn delete_team(pool: &Pool, id: i64) -> sqlx::Result<()> {
405 -
    sqlx::query("DELETE FROM teams WHERE id = ?").bind(id).execute(pool).await?;
405 +
    sqlx::query("DELETE FROM teams WHERE id = $1").bind(id).execute(pool).await?;
406 406
    Ok(())
407 407
}
408 408
409 409
pub async fn team_members(pool: &Pool, team_id: i64) -> sqlx::Result<Vec<Account>> {
410 -
    sqlx::query_as("SELECT a.* FROM accounts a JOIN team_members t ON t.account_id = a.id WHERE t.team_id = ? ORDER BY a.username")
410 +
    sqlx::query_as("SELECT a.* FROM accounts a JOIN team_members t ON t.account_id = a.id WHERE t.team_id = $1 ORDER BY a.username")
411 411
        .bind(team_id)
412 412
        .fetch_all(pool)
413 413
        .await
414 414
}
415 415
416 416
pub async fn add_team_member(pool: &Pool, team_id: i64, account_id: i64) -> sqlx::Result<()> {
417 -
    sqlx::query("INSERT OR IGNORE INTO team_members (team_id, account_id) VALUES (?, ?)")
417 +
    sqlx::query("INSERT OR IGNORE INTO team_members (team_id, account_id) VALUES ($1, $2)")
418 418
        .bind(team_id)
419 419
        .bind(account_id)
420 420
        .execute(pool)
…
2 unchanged lines
423 423
}
424 424
425 425
pub async fn remove_team_member(pool: &Pool, team_id: i64, account_id: i64) -> sqlx::Result<()> {
426 -
    sqlx::query("DELETE FROM team_members WHERE team_id = ? AND account_id = ?").bind(team_id).bind(account_id).execute(pool).await?;
426 +
    sqlx::query("DELETE FROM team_members WHERE team_id = $1 AND account_id = $2").bind(team_id).bind(account_id).execute(pool).await?;
427 427
    Ok(())
428 428
}
429 429
…
6 unchanged lines
436 436
pub async fn assignable(pool: &Pool, repo_id: i64, owner: &str) -> sqlx::Result<Vec<String>> {
437 437
    sqlx::query_scalar(
438 438
        "SELECT username FROM accounts WHERE banned = 0 AND disabled = 0 AND id IN (\
439 -
           SELECT id FROM accounts WHERE username = ?1 \
440 -
           UNION SELECT account_id FROM collaborators WHERE repo_id = ?2 AND account_id IS NOT NULL \
441 -
           UNION SELECT tm.account_id FROM team_members tm JOIN collaborators c ON c.team_id = tm.team_id WHERE c.repo_id = ?2 \
442 -
           UNION SELECT m.account_id FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = ?1) \
439 +
           SELECT id FROM accounts WHERE username = $1 \
440 +
           UNION SELECT account_id FROM collaborators WHERE repo_id = $2 AND account_id IS NOT NULL \
441 +
           UNION SELECT tm.account_id FROM team_members tm JOIN collaborators c ON c.team_id = tm.team_id WHERE c.repo_id = $2 \
442 +
           UNION SELECT m.account_id FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = $1) \
443 443
         ORDER BY username COLLATE NOCASE",
444 444
    )
445 445
    .bind(owner)
…
11 unchanged lines
457 457
458 458
pub async fn collaborators(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Collaborator>> {
459 459
    let rows: Vec<(Option<i64>, Option<i64>, String)> =
460 -
        sqlx::query_as("SELECT account_id, team_id, role FROM collaborators WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await?;
460 +
        sqlx::query_as("SELECT account_id, team_id, role FROM collaborators WHERE repo_id = $1").bind(repo_id).fetch_all(pool).await?;
461 461
    let mut out = Vec::new();
462 462
    for (account_id, team_id, role) in rows {
463 463
        let account = match account_id {
…
1 unchanged lines
465 465
            None => None,
466 466
        };
467 467
        let team = match team_id {
468 -
            Some(id) => sqlx::query_as("SELECT * FROM teams WHERE id = ?").bind(id).fetch_optional(pool).await?,
468 +
            Some(id) => sqlx::query_as("SELECT * FROM teams WHERE id = $1").bind(id).fetch_optional(pool).await?,
469 469
            None => None,
470 470
        };
471 471
        out.push(Collaborator { account, team, role });
…
3 unchanged lines
475 475
476 476
pub async fn set_collaborator(pool: &Pool, repo_id: i64, account_id: Option<i64>, team_id: Option<i64>, role: &str) -> sqlx::Result<()> {
477 477
    if let Some(a) = account_id {
478 -
        sqlx::query("INSERT INTO collaborators (repo_id, account_id, role) VALUES (?, ?, ?) ON CONFLICT (repo_id, account_id) WHERE account_id IS NOT NULL DO UPDATE SET role = excluded.role")
478 +
        sqlx::query("INSERT INTO collaborators (repo_id, account_id, role) VALUES ($1, $2, $3) ON CONFLICT (repo_id, account_id) WHERE account_id IS NOT NULL DO UPDATE SET role = excluded.role")
479 479
            .bind(repo_id)
480 480
            .bind(a)
481 481
            .bind(role)
482 482
            .execute(pool)
483 483
            .await?;
484 484
    } else if let Some(t) = team_id {
485 -
        sqlx::query("INSERT INTO collaborators (repo_id, team_id, role) VALUES (?, ?, ?) ON CONFLICT (repo_id, team_id) WHERE team_id IS NOT NULL DO UPDATE SET role = excluded.role")
485 +
        sqlx::query("INSERT INTO collaborators (repo_id, team_id, role) VALUES ($1, $2, $3) ON CONFLICT (repo_id, team_id) WHERE team_id IS NOT NULL DO UPDATE SET role = excluded.role")
486 486
            .bind(repo_id)
487 487
            .bind(t)
488 488
            .bind(role)
…
4 unchanged lines
493 493
}
494 494
495 495
pub async fn remove_collaborator(pool: &Pool, repo_id: i64, account_id: Option<i64>, team_id: Option<i64>) -> sqlx::Result<()> {
496 -
    sqlx::query("DELETE FROM collaborators WHERE repo_id = ? AND ((account_id IS NOT NULL AND account_id = ?) OR (team_id IS NOT NULL AND team_id = ?))")
496 +
    sqlx::query("DELETE FROM collaborators WHERE repo_id = $1 AND ((account_id IS NOT NULL AND account_id = $2) OR (team_id IS NOT NULL AND team_id = $3))")
497 497
        .bind(repo_id)
498 498
        .bind(account_id)
499 499
        .bind(team_id)
…
5 unchanged lines
505 505
/// The direct or team role an account holds on a repository, the highest.
506 506
pub async fn collaborator_role(pool: &Pool, repo_id: i64, account_id: i64) -> sqlx::Result<Option<String>> {
507 507
    let rows: Vec<String> = sqlx::query_scalar(
508 -
        "SELECT role FROM collaborators WHERE repo_id = ? AND (account_id = ? OR team_id IN (SELECT team_id FROM team_members WHERE account_id = ?))",
508 +
        "SELECT role FROM collaborators WHERE repo_id = $1 AND (account_id = $2 OR team_id IN (SELECT team_id FROM team_members WHERE account_id = $3))",
509 509
    )
510 510
    .bind(repo_id)
511 511
    .bind(account_id)
…
6 unchanged lines
518 518
// ------------------------------------------------------------------ tokens
519 519
520 520
pub async fn tokens_of(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<Token>> {
521 -
    sqlx::query_as("SELECT * FROM tokens WHERE account_id = ? ORDER BY created_ms DESC").bind(account_id).fetch_all(pool).await
521 +
    sqlx::query_as("SELECT * FROM tokens WHERE account_id = $1 ORDER BY created_ms DESC").bind(account_id).fetch_all(pool).await
522 522
}
523 523
524 524
/// Mark one of `account_id`'s tokens as an agent's, or not.
525 525
pub async fn set_token_agent(pool: &Pool, account_id: i64, id: i64, agent: bool) -> sqlx::Result<bool> {
526 -
    let r = sqlx::query("UPDATE tokens SET agent = ? WHERE id = ? AND account_id = ?")
526 +
    let r = sqlx::query("UPDATE tokens SET agent = $1 WHERE id = $2 AND account_id = $3")
527 527
        .bind(agent)
528 528
        .bind(id)
529 529
        .bind(account_id)
…
3 unchanged lines
533 533
}
534 534
535 535
pub async fn token_by_hash(pool: &Pool, hash: &str) -> sqlx::Result<Option<Token>> {
536 -
    sqlx::query_as("SELECT * FROM tokens WHERE hash = ?").bind(hash).fetch_optional(pool).await
536 +
    sqlx::query_as("SELECT * FROM tokens WHERE hash = $1").bind(hash).fetch_optional(pool).await
537 537
}
538 538
539 539
pub async fn create_token(
…
6 unchanged lines
546 546
    expires_ms: Option<i64>,
547 547
) -> sqlx::Result<Token> {
548 548
    sqlx::query_as(
549 -
        "INSERT INTO tokens (account_id, name, hash, scopes, repos, created_ms, expires_ms) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *",
549 +
        "INSERT INTO tokens (account_id, name, hash, scopes, repos, created_ms, expires_ms) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
550 550
    )
551 551
    .bind(account_id)
552 552
    .bind(name)
…
7 unchanged lines
560 560
}
561 561
562 562
pub async fn delete_token(pool: &Pool, account_id: i64, id: i64) -> sqlx::Result<()> {
563 -
    sqlx::query("DELETE FROM tokens WHERE id = ? AND account_id = ?").bind(id).bind(account_id).execute(pool).await?;
563 +
    sqlx::query("DELETE FROM tokens WHERE id = $1 AND account_id = $2").bind(id).bind(account_id).execute(pool).await?;
564 564
    Ok(())
565 565
}
566 566
567 567
/// Record a use; a renewing token's expiry moves to now plus its lifetime.
568 568
pub async fn touch_token(pool: &Pool, id: i64) -> sqlx::Result<()> {
569 569
    sqlx::query(
570 -
        "UPDATE tokens SET last_used_ms = ?1, \
571 -
         expires_ms = CASE WHEN renew_ms IS NULL THEN expires_ms ELSE MAX(COALESCE(expires_ms, 0), ?1 + renew_ms) END \
572 -
         WHERE id = ?2",
570 +
        "UPDATE tokens SET last_used_ms = $1, \
571 +
         expires_ms = CASE WHEN renew_ms IS NULL THEN expires_ms ELSE MAX(COALESCE(expires_ms, 0), $1 + renew_ms) END \
572 +
         WHERE id = $2",
573 573
    )
574 574
    .bind(now_ms())
575 575
    .bind(id)
…
4 unchanged lines
580 580
581 581
/// Make a token renewing: it lasts `renew_ms` past its last use.
582 582
pub async fn set_token_renewal(pool: &Pool, id: i64, renew_ms: i64) -> sqlx::Result<()> {
583 -
    sqlx::query("UPDATE tokens SET renew_ms = ? WHERE id = ?").bind(renew_ms).bind(id).execute(pool).await?;
583 +
    sqlx::query("UPDATE tokens SET renew_ms = $1 WHERE id = $2").bind(renew_ms).bind(id).execute(pool).await?;
584 584
    Ok(())
585 585
}
586 586
…
34 unchanged lines
621 621
    requester: &str,
622 622
) -> sqlx::Result<DeviceCode> {
623 623
    // Expired codes go whenever a new one is made.
624 -
    sqlx::query("DELETE FROM device_codes WHERE expires_ms < ?").bind(now_ms()).execute(pool).await?;
624 +
    sqlx::query("DELETE FROM device_codes WHERE expires_ms < $1").bind(now_ms()).execute(pool).await?;
625 625
    sqlx::query_as(
626 -
        "INSERT INTO device_codes (device_hash, user_code, client, scopes, created_ms, expires_ms, requester) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *",
626 +
        "INSERT INTO device_codes (device_hash, user_code, client, scopes, created_ms, expires_ms, requester) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
627 627
    )
628 628
    .bind(device_hash)
629 629
    .bind(user_code)
…
7 unchanged lines
637 637
}
638 638
639 639
pub async fn device_by_user_code(pool: &Pool, user_code: &str) -> sqlx::Result<Option<DeviceCode>> {
640 -
    sqlx::query_as("SELECT * FROM device_codes WHERE user_code = ?").bind(user_code).fetch_optional(pool).await
640 +
    sqlx::query_as("SELECT * FROM device_codes WHERE user_code = $1").bind(user_code).fetch_optional(pool).await
641 641
}
642 642
643 643
pub async fn device_by_hash(pool: &Pool, device_hash: &str) -> sqlx::Result<Option<DeviceCode>> {
644 -
    sqlx::query_as("SELECT * FROM device_codes WHERE device_hash = ?").bind(device_hash).fetch_optional(pool).await
644 +
    sqlx::query_as("SELECT * FROM device_codes WHERE device_hash = $1").bind(device_hash).fetch_optional(pool).await
645 645
}
646 646
647 647
/// Approve a pending code for an account with these scopes; false when it
648 648
/// was no longer pending.
649 649
pub async fn approve_device(pool: &Pool, id: i64, account_id: i64, scopes: &str) -> sqlx::Result<bool> {
650 650
    let r = sqlx::query(
651 -
        "UPDATE device_codes SET account_id = ?, approved_scopes = ? WHERE id = ? AND account_id IS NULL AND denied = 0 AND expires_ms >= ?",
651 +
        "UPDATE device_codes SET account_id = $1, approved_scopes = $2 WHERE id = $3 AND account_id IS NULL AND denied = 0 AND expires_ms >= $4",
652 652
    )
653 653
    .bind(account_id)
654 654
    .bind(scopes)
…
5 unchanged lines
660 660
}
661 661
662 662
pub async fn deny_device(pool: &Pool, id: i64) -> sqlx::Result<()> {
663 -
    sqlx::query("UPDATE device_codes SET denied = 1 WHERE id = ? AND account_id IS NULL").bind(id).execute(pool).await?;
663 +
    sqlx::query("UPDATE device_codes SET denied = 1 WHERE id = $1 AND account_id IS NULL").bind(id).execute(pool).await?;
664 664
    Ok(())
665 665
}
666 666
667 667
pub async fn device_polled(pool: &Pool, id: i64) -> sqlx::Result<()> {
668 -
    sqlx::query("UPDATE device_codes SET last_poll_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
668 +
    sqlx::query("UPDATE device_codes SET last_poll_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
669 669
    Ok(())
670 670
}
671 671
672 672
/// Remove a code; true when this call removed it (so one poll gets the
673 673
/// token, however many race).
674 674
pub async fn delete_device(pool: &Pool, id: i64) -> sqlx::Result<bool> {
675 -
    let r = sqlx::query("DELETE FROM device_codes WHERE id = ?").bind(id).execute(pool).await?;
675 +
    let r = sqlx::query("DELETE FROM device_codes WHERE id = $1").bind(id).execute(pool).await?;
676 676
    Ok(r.rows_affected() == 1)
677 677
}
678 678
679 679
// -------------------------------------------------------------- identities
680 680
681 681
pub async fn identities_of(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<LinkedIdentity>> {
682 -
    sqlx::query_as("SELECT * FROM identities WHERE account_id = ? ORDER BY linked_ms DESC").bind(account_id).fetch_all(pool).await
682 +
    sqlx::query_as("SELECT * FROM identities WHERE account_id = $1 ORDER BY linked_ms DESC").bind(account_id).fetch_all(pool).await
683 683
}
684 684
685 685
pub async fn identity_by_key(pool: &Pool, key: &str) -> sqlx::Result<Option<LinkedIdentity>> {
686 -
    sqlx::query_as("SELECT * FROM identities WHERE key = ?").bind(key).fetch_optional(pool).await
686 +
    sqlx::query_as("SELECT * FROM identities WHERE key = $1").bind(key).fetch_optional(pool).await
687 687
}
688 688
689 689
/// Every linked key with its account's username, for attribution.
…
2 unchanged lines
692 692
}
693 693
694 694
pub async fn link_identity(pool: &Pool, account_id: i64, key: &str, name: &str, email: &str, object: &[u8]) -> sqlx::Result<()> {
695 -
    sqlx::query("INSERT INTO identities (key, account_id, name, email, object, linked_ms) VALUES (?, ?, ?, ?, ?, ?)")
695 +
    sqlx::query("INSERT INTO identities (key, account_id, name, email, object, linked_ms) VALUES ($1, $2, $3, $4, $5, $6)")
696 696
        .bind(key)
697 697
        .bind(account_id)
698 698
        .bind(name)
…
6 unchanged lines
705 705
}
706 706
707 707
pub async fn unlink_identity(pool: &Pool, account_id: i64, key: &str) -> sqlx::Result<()> {
708 -
    sqlx::query("DELETE FROM identities WHERE key = ? AND account_id = ?").bind(key).bind(account_id).execute(pool).await?;
708 +
    sqlx::query("DELETE FROM identities WHERE key = $1 AND account_id = $2").bind(key).bind(account_id).execute(pool).await?;
709 709
    Ok(())
710 710
}
711 711
712 712
pub async fn create_challenge(pool: &Pool, account_id: i64, bytes_hex: &str) -> sqlx::Result<i64> {
713 -
    sqlx::query("DELETE FROM challenges WHERE created_ms < ?").bind(now_ms() - 10 * 60 * 1000).execute(pool).await?;
714 -
    let id: i64 = sqlx::query_scalar("INSERT INTO challenges (account_id, bytes, created_ms) VALUES (?, ?, ?) RETURNING id")
713 +
    sqlx::query("DELETE FROM challenges WHERE created_ms < $1").bind(now_ms() - 10 * 60 * 1000).execute(pool).await?;
714 +
    let id: i64 = sqlx::query_scalar("INSERT INTO challenges (account_id, bytes, created_ms) VALUES ($1, $2, $3) RETURNING id")
715 715
        .bind(account_id)
716 716
        .bind(bytes_hex)
717 717
        .bind(now_ms())
…
4 unchanged lines
722 722
723 723
/// Take (and delete) a pending challenge of this account.
724 724
pub async fn take_challenge(pool: &Pool, account_id: i64, id: i64) -> sqlx::Result<Option<String>> {
725 -
    let bytes: Option<String> = sqlx::query_scalar("SELECT bytes FROM challenges WHERE id = ? AND account_id = ? AND created_ms >= ?")
725 +
    let bytes: Option<String> = sqlx::query_scalar("SELECT bytes FROM challenges WHERE id = $1 AND account_id = $2 AND created_ms >= $3")
726 726
        .bind(id)
727 727
        .bind(account_id)
728 728
        .bind(now_ms() - 10 * 60 * 1000)
729 729
        .fetch_optional(pool)
730 730
        .await?;
731 -
    sqlx::query("DELETE FROM challenges WHERE id = ?").bind(id).execute(pool).await?;
731 +
    sqlx::query("DELETE FROM challenges WHERE id = $1").bind(id).execute(pool).await?;
732 732
    Ok(bytes)
733 733
}
734 734
735 735
// ------------------------------------------------------------- invitations
736 736
737 737
pub async fn create_invitation(pool: &Pool, code: &str, created_by: Option<i64>) -> sqlx::Result<()> {
738 -
    sqlx::query("INSERT INTO invitations (code, created_by, created_ms) VALUES (?, ?, ?)")
738 +
    sqlx::query("INSERT INTO invitations (code, created_by, created_ms) VALUES ($1, $2, $3)")
739 739
        .bind(code)
740 740
        .bind(created_by)
741 741
        .bind(now_ms())
…
4 unchanged lines
746 746
747 747
pub async fn invitation_unused(pool: &Pool, code: &str) -> sqlx::Result<bool> {
748 748
    let n: i64 =
749 -
        sqlx::query_scalar("SELECT COUNT(*) FROM invitations WHERE code = ? AND used_by IS NULL").bind(code).fetch_one(pool).await?;
749 +
        sqlx::query_scalar("SELECT COUNT(*) FROM invitations WHERE code = $1 AND used_by IS NULL").bind(code).fetch_one(pool).await?;
750 750
    Ok(n > 0)
751 751
}
752 752
753 753
/// Use an invitation up if it is still unused; whether it was.
754 754
pub async fn claim_invitation(pool: &Pool, code: &str, account_id: i64) -> sqlx::Result<bool> {
755 -
    let r = sqlx::query("UPDATE invitations SET used_by = ? WHERE code = ? AND used_by IS NULL")
755 +
    let r = sqlx::query("UPDATE invitations SET used_by = $1 WHERE code = $2 AND used_by IS NULL")
756 756
        .bind(account_id)
757 757
        .bind(code)
758 758
        .execute(pool)
…
2 unchanged lines
761 761
}
762 762
763 763
pub async fn use_invitation(pool: &Pool, code: &str, account_id: i64) -> sqlx::Result<()> {
764 -
    sqlx::query("UPDATE invitations SET used_by = ? WHERE code = ?").bind(account_id).bind(code).execute(pool).await?;
764 +
    sqlx::query("UPDATE invitations SET used_by = $1 WHERE code = $2").bind(account_id).bind(code).execute(pool).await?;
765 765
    Ok(())
766 766
}
767 767
…
4 unchanged lines
772 772
// ---------------------------------------------------------------- settings
773 773
774 774
pub async fn setting(pool: &Pool, key: &str) -> sqlx::Result<Option<String>> {
775 -
    sqlx::query_scalar("SELECT value FROM settings WHERE key = ?").bind(key).fetch_optional(pool).await
775 +
    sqlx::query_scalar("SELECT value FROM settings WHERE key = $1").bind(key).fetch_optional(pool).await
776 776
}
777 777
778 778
pub async fn set_setting(pool: &Pool, key: &str, value: &str) -> sqlx::Result<()> {
779 -
    sqlx::query("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value")
779 +
    sqlx::query("INSERT INTO settings (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = excluded.value")
780 780
        .bind(key)
781 781
        .bind(value)
782 782
        .execute(pool)
…
4 unchanged lines
787 787
// ------------------------------------------------------------------- audit
788 788
789 789
pub async fn audit(pool: &Pool, actor: Option<&Account>, action: &str, target: &str, detail: &str) -> sqlx::Result<()> {
790 -
    sqlx::query("INSERT INTO audit (at_ms, actor_id, actor, action, target, detail) VALUES (?, ?, ?, ?, ?, ?)")
790 +
    sqlx::query("INSERT INTO audit (at_ms, actor_id, actor, action, target, detail) VALUES ($1, $2, $3, $4, $5, $6)")
791 791
        .bind(now_ms())
792 792
        .bind(actor.map(|a| a.id))
793 793
        .bind(actor.map(|a| a.username.as_str()).unwrap_or(""))
…
7 unchanged lines
801 801
802 802
/// A page of the audit log, newest first, older than `before` (an id) when given.
803 803
pub async fn audit_page(pool: &Pool, before: Option<i64>, limit: i64) -> sqlx::Result<Vec<AuditRow>> {
804 -
    sqlx::query_as("SELECT id, at_ms, actor, action, target, detail FROM audit WHERE (? IS NULL OR id < ?) ORDER BY id DESC LIMIT ?")
804 +
    sqlx::query_as("SELECT id, at_ms, actor, action, target, detail FROM audit WHERE ($1 IS NULL OR id < $2) ORDER BY id DESC LIMIT $3")
805 805
        .bind(before)
806 806
        .bind(before)
807 807
        .bind(limit)
…
2 unchanged lines
810 810
}
811 811
812 812
pub async fn audit_rows(pool: &Pool, limit: i64) -> sqlx::Result<Vec<AuditRow>> {
813 -
    sqlx::query_as("SELECT id, at_ms, actor, action, target, detail FROM audit ORDER BY at_ms DESC, id DESC LIMIT ?")
813 +
    sqlx::query_as("SELECT id, at_ms, actor, action, target, detail FROM audit ORDER BY at_ms DESC, id DESC LIMIT $1")
814 814
        .bind(limit)
815 815
        .fetch_all(pool)
816 816
        .await
…
3 unchanged lines
820 820
/// its organisations', and those it collaborates on.
821 821
pub async fn visible_repo_ids(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<i64>> {
822 822
    sqlx::query_scalar(
823 -
        "SELECT r.id FROM repositories r WHERE r.owner IN (SELECT username FROM accounts WHERE id = ?) \
824 -
         OR r.owner IN (SELECT o.name FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = ?) \
825 -
         OR r.id IN (SELECT repo_id FROM collaborators WHERE account_id = ? OR team_id IN (SELECT team_id FROM team_members WHERE account_id = ?))",
823 +
        "SELECT r.id FROM repositories r WHERE r.owner IN (SELECT username FROM accounts WHERE id = $1) \
824 +
         OR r.owner IN (SELECT o.name FROM organisations o JOIN org_members m ON m.org_id = o.id WHERE m.account_id = $2) \
825 +
         OR r.id IN (SELECT repo_id FROM collaborators WHERE account_id = $3 OR team_id IN (SELECT team_id FROM team_members WHERE account_id = $4))",
826 826
    )
827 827
    .bind(account_id)
828 828
    .bind(account_id)
…
31 unchanged lines
860 860
    let secret = crate::auth::random_hex(32);
861 861
    let now = now_ms();
862 862
    sqlx::query(
863 -
        "INSERT INTO account_links (account_id, kind, hash, email, created_by, created_ms, expires_ms) VALUES (?, ?, ?, ?, ?, ?, ?)",
863 +
        "INSERT INTO account_links (account_id, kind, hash, email, created_by, created_ms, expires_ms) VALUES ($1, $2, $3, $4, $5, $6, $7)",
864 864
    )
865 865
    .bind(account_id)
866 866
    .bind(kind)
…
9 unchanged lines
876 876
877 877
/// An unused, unexpired link of this kind with this secret.
878 878
pub async fn live_link(pool: &Pool, kind: &str, secret: &str) -> sqlx::Result<Option<AccountLink>> {
879 -
    sqlx::query_as("SELECT * FROM account_links WHERE kind = ? AND hash = ? AND used_ms IS NULL AND expires_ms > ?")
879 +
    sqlx::query_as("SELECT * FROM account_links WHERE kind = $1 AND hash = $2 AND used_ms IS NULL AND expires_ms > $3")
880 880
        .bind(kind)
881 881
        .bind(crate::auth::token_hash(secret))
882 882
        .bind(now_ms())
…
3 unchanged lines
886 886
887 887
/// Consume a link; false when it was already used (a race lost).
888 888
pub async fn use_link(pool: &Pool, id: i64) -> sqlx::Result<bool> {
889 -
    let r =
890 -
        sqlx::query("UPDATE account_links SET used_ms = ? WHERE id = ? AND used_ms IS NULL").bind(now_ms()).bind(id).execute(pool).await?;
889 +
    let r = sqlx::query("UPDATE account_links SET used_ms = $1 WHERE id = $2 AND used_ms IS NULL")
890 +
        .bind(now_ms())
891 +
        .bind(id)
892 +
        .execute(pool)
893 +
        .await?;
891 894
    Ok(r.rows_affected() == 1)
892 895
}
893 896
894 897
/// Void every unused link of a kind for an account.
895 898
pub async fn void_links(pool: &Pool, account_id: i64, kind: &str) -> sqlx::Result<()> {
896 -
    sqlx::query("UPDATE account_links SET used_ms = ? WHERE account_id = ? AND kind = ? AND used_ms IS NULL")
899 +
    sqlx::query("UPDATE account_links SET used_ms = $1 WHERE account_id = $2 AND kind = $3 AND used_ms IS NULL")
897 900
        .bind(now_ms())
898 901
        .bind(account_id)
899 902
        .bind(kind)
…
6 unchanged lines
906 909
pub async fn last_link_ms(pool: &Pool, account_id: i64, kind: &str, email: Option<&str>) -> sqlx::Result<Option<i64>> {
907 910
    match email {
908 911
        Some(e) => {
909 -
            sqlx::query_scalar("SELECT MAX(created_ms) FROM account_links WHERE account_id = ? AND kind = ? AND email = ?")
912 +
            sqlx::query_scalar("SELECT MAX(created_ms) FROM account_links WHERE account_id = $1 AND kind = $2 AND email = $3")
910 913
                .bind(account_id)
911 914
                .bind(kind)
912 915
                .bind(e)
…
1 unchanged lines
914 917
                .await
915 918
        }
916 919
        None => {
917 -
            sqlx::query_scalar("SELECT MAX(created_ms) FROM account_links WHERE account_id = ? AND kind = ?")
920 +
            sqlx::query_scalar("SELECT MAX(created_ms) FROM account_links WHERE account_id = $1 AND kind = $2")
918 921
                .bind(account_id)
919 922
                .bind(kind)
920 923
                .fetch_one(pool)
…
4 unchanged lines
925 928
926 929
/// How many links of a kind the account got since `since_ms`.
927 930
pub async fn links_since(pool: &Pool, account_id: i64, kind: &str, since_ms: i64) -> sqlx::Result<i64> {
928 -
    sqlx::query_scalar("SELECT COUNT(*) FROM account_links WHERE account_id = ? AND kind = ? AND created_ms >= ?")
931 +
    sqlx::query_scalar("SELECT COUNT(*) FROM account_links WHERE account_id = $1 AND kind = $2 AND created_ms >= $3")
929 932
        .bind(account_id)
930 933
        .bind(kind)
931 934
        .bind(since_ms)
…
2 unchanged lines
934 937
}
935 938
936 939
pub async fn set_email_verified(pool: &Pool, id: i64, verified: bool) -> sqlx::Result<()> {
937 -
    sqlx::query("UPDATE accounts SET email_verified = ? WHERE id = ?").bind(verified).bind(id).execute(pool).await?;
940 +
    sqlx::query("UPDATE accounts SET email_verified = $1 WHERE id = $2").bind(verified).bind(id).execute(pool).await?;
938 941
    Ok(())
939 942
}
940 943
941 944
/// The account with this verified email (case-insensitive), if exactly one.
942 945
pub async fn account_by_verified_email(pool: &Pool, email: &str) -> sqlx::Result<Option<Account>> {
943 -
    let found: Vec<Account> = sqlx::query_as("SELECT * FROM accounts WHERE email_verified = 1 AND lower(email) = lower(?) LIMIT 2")
946 +
    let found: Vec<Account> = sqlx::query_as("SELECT * FROM accounts WHERE email_verified = 1 AND lower(email) = lower($1) LIMIT 2")
944 947
        .bind(email)
945 948
        .fetch_all(pool)
946 949
        .await?;
…
3 unchanged lines
950 953
/// Set or clear the picture of an account (`account`) or organisation (`org`).
951 954
pub async fn set_avatar(pool: &Pool, kind: &str, id: i64, value: &str) -> sqlx::Result<()> {
952 955
    let table = if kind == "org" { "organisations" } else { "accounts" };
953 -
    sqlx::query(&format!("UPDATE {table} SET avatar = ? WHERE id = ?")).bind(value).bind(id).execute(pool).await?;
956 +
    sqlx::query(&format!("UPDATE {table} SET avatar = $1 WHERE id = $2")).bind(value).bind(id).execute(pool).await?;
954 957
    Ok(())
955 958
}
modified crates/hub/src/db/activity.rs +15 −15
…
47 unchanged lines
48 48
/// Like `feed`, only what `actor` did when one is given (profile pages).
49 49
pub async fn feed_by(pool: &Pool, repo_ids: &[i64], actor: Option<&str>, limit: i64) -> sqlx::Result<Vec<Activity>> {
50 50
    sqlx::query_as(
51 -
        "WITH seen(id) AS (SELECT value FROM json_each(?)), ev AS ( \
51 +
        "WITH seen(id) AS (SELECT value FROM json_each($1)), ev AS ( \
52 52
           SELECT repo_id, at_ms, pusher AS actor, 'push' AS kind, reference AS event, '' AS title, 0 AS number FROM pushes WHERE ok = 1 \
53 53
           UNION ALL SELECT repo_id, created_ms, author, 'release', 'published', tag, 0 FROM releases \
54 54
           UNION ALL SELECT p.repo_id, e.at_ms, e.actor, 'proposal', e.event, p.title, p.number FROM proposal_events e JOIN proposals p ON p.id = e.proposal_id \
55 55
           UNION ALL SELECT i.repo_id, e.at_ms, e.actor, 'issue', e.event, i.title, i.number FROM issue_events e JOIN issues i ON i.id = e.issue_id) \
56 56
         SELECT r.owner, r.name, ev.at_ms, ev.actor, ev.kind, ev.event, ev.title, ev.number \
57 57
         FROM ev JOIN repositories r ON r.id = ev.repo_id WHERE ev.repo_id IN (SELECT id FROM seen) \
58 -
           AND (? IS NULL OR ev.actor = ? COLLATE NOCASE) \
59 -
         ORDER BY ev.at_ms DESC LIMIT ?",
58 +
           AND ($2 IS NULL OR ev.actor = $3 COLLATE NOCASE) \
59 +
         ORDER BY ev.at_ms DESC LIMIT $4",
60 60
    )
61 61
    .bind(ids_json(repo_ids))
62 62
    .bind(actor)
…
22 unchanged lines
85 85
    sqlx::query_as(
86 86
        "SELECT r.owner, r.name, p.number, p.title, p.author, p.state, p.conflicted, p.updated_ms \
87 87
         FROM proposals p JOIN repositories r ON r.id = p.repo_id \
88 -
         WHERE p.repo_id IN (SELECT value FROM json_each(?)) AND p.state = 'open' \
89 -
           AND (p.author_id IS NULL OR p.author_id != ?) \
90 -
           AND NOT EXISTS (SELECT 1 FROM proposal_reviews v WHERE v.proposal_id = p.id AND v.author_id = ?) \
88 +
         WHERE p.repo_id IN (SELECT value FROM json_each($1)) AND p.state = 'open' \
89 +
           AND (p.author_id IS NULL OR p.author_id != $2) \
90 +
           AND NOT EXISTS (SELECT 1 FROM proposal_reviews v WHERE v.proposal_id = p.id AND v.author_id = $3) \
91 91
         ORDER BY p.updated_ms DESC",
92 92
    )
93 93
    .bind(ids_json(repo_ids))
…
9 unchanged lines
103 103
    sqlx::query_as(
104 104
        "SELECT r.owner, r.name, p.number, p.title, p.author, p.state, p.conflicted, p.updated_ms \
105 105
         FROM proposals p JOIN repositories r ON r.id = p.repo_id \
106 -
         WHERE p.author_id = ? AND p.state IN ('open', 'draft') AND p.repo_id IN (SELECT value FROM json_each(?)) \
106 +
         WHERE p.author_id = $1 AND p.state IN ('open', 'draft') AND p.repo_id IN (SELECT value FROM json_each($2)) \
107 107
         ORDER BY p.updated_ms DESC",
108 108
    )
109 109
    .bind(account_id)
…
17 unchanged lines
127 127
    sqlx::query_as(
128 128
        "SELECT r.owner, r.name, i.number, i.title, i.updated_ms \
129 129
         FROM issues i JOIN issue_assignees a ON a.issue_id = i.id JOIN repositories r ON r.id = i.repo_id \
130 -
         WHERE a.account_id = ? AND i.state = 'open' AND i.repo_id IN (SELECT value FROM json_each(?)) \
130 +
         WHERE a.account_id = $1 AND i.state = 'open' AND i.repo_id IN (SELECT value FROM json_each($2)) \
131 131
         ORDER BY i.updated_ms DESC",
132 132
    )
133 133
    .bind(account_id)
…
16 unchanged lines
150 150
/// The events since `since_ms` in `repo_ids`, newest first, at most `limit`.
151 151
pub async fn sky(pool: &Pool, repo_ids: &[i64], since_ms: i64, limit: i64) -> sqlx::Result<Vec<SkyEvent>> {
152 152
    sqlx::query_as(
153 -
        "WITH seen(id) AS (SELECT value FROM json_each(?)), ev AS ( \
153 +
        "WITH seen(id) AS (SELECT value FROM json_each($1)), ev AS ( \
154 154
           SELECT repo_id, at_ms, 0 AS kind, MAX(changes, 1) AS n FROM pushes WHERE ok = 1 \
155 155
           UNION ALL SELECT p.repo_id, e.at_ms, 1, 1 FROM proposal_events e JOIN proposals p ON p.id = e.proposal_id \
156 156
           UNION ALL SELECT i.repo_id, e.at_ms, 2, 1 FROM issue_events e JOIN issues i ON i.id = e.issue_id \
157 157
           UNION ALL SELECT repo_id, created_ms, 3, 1 FROM releases \
158 158
           UNION ALL SELECT repo_id, created_ms, 4, 1 FROM flow_runs) \
159 -
         SELECT repo_id, at_ms, kind, n FROM ev WHERE repo_id IN (SELECT id FROM seen) AND at_ms >= ? \
160 -
         ORDER BY at_ms DESC LIMIT ?",
159 +
         SELECT repo_id, at_ms, kind, n FROM ev WHERE repo_id IN (SELECT id FROM seen) AND at_ms >= $2 \
160 +
         ORDER BY at_ms DESC LIMIT $3",
161 161
    )
162 162
    .bind(ids_json(repo_ids))
163 163
    .bind(since_ms)
…
10 unchanged lines
174 174
    let ids = serde_json::to_string(repo_ids).unwrap_or_else(|_| "[]".into());
175 175
    sqlx::query_as(
176 176
        "SELECT repo_id, at_ms, changes, via FROM pushes \
177 -
         WHERE account_id = ? AND ok = 1 AND changes > 0 AND at_ms >= ? AND reason NOT LIKE 'landed%' \
178 -
           AND repo_id IN (SELECT value FROM json_each(?)) ORDER BY at_ms",
177 +
         WHERE account_id = $1 AND ok = 1 AND changes > 0 AND at_ms >= $2 AND reason NOT LIKE 'landed%' \
178 +
           AND repo_id IN (SELECT value FROM json_each($3)) ORDER BY at_ms",
179 179
    )
180 180
    .bind(account_id)
181 181
    .bind(since_ms)
…
7 unchanged lines
189 189
pub async fn releases_by(pool: &Pool, account_id: i64, repo_ids: &[i64], since_ms: i64) -> sqlx::Result<Vec<(i64, String, i64)>> {
190 190
    let ids = serde_json::to_string(repo_ids).unwrap_or_else(|_| "[]".into());
191 191
    sqlx::query_as(
192 -
        "SELECT repo_id, tag, created_ms FROM releases WHERE author_id = ? AND created_ms >= ? \
193 -
           AND repo_id IN (SELECT value FROM json_each(?)) ORDER BY created_ms",
192 +
        "SELECT repo_id, tag, created_ms FROM releases WHERE author_id = $1 AND created_ms >= $2 \
193 +
           AND repo_id IN (SELECT value FROM json_each($3)) ORDER BY created_ms",
194 194
    )
195 195
    .bind(account_id)
196 196
    .bind(since_ms)
…
4 unchanged lines
modified crates/hub/src/db/flow.rs +76 −74
…
146 unchanged lines
147 147
pub async fn learned_disk_need(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<Option<i64>> {
148 148
    let peak: Option<i64> = sqlx::query_scalar(
149 149
        "SELECT MAX(peak_disk_mib) FROM (SELECT j.peak_disk_mib FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id \
150 -
         WHERE r.repo_id = ? AND j.name = ? AND j.peak_disk_mib IS NOT NULL ORDER BY j.id DESC LIMIT ?)",
150 +
         WHERE r.repo_id = $1 AND j.name = $2 AND j.peak_disk_mib IS NOT NULL ORDER BY j.id DESC LIMIT $3)",
151 151
    )
152 152
    .bind(repo_id)
153 153
    .bind(name)
…
5 unchanged lines
159 159
160 160
/// Record why job `job_id` ran where it did.
161 161
pub async fn set_placed(pool: &Pool, job_id: i64, placed: &str) -> sqlx::Result<()> {
162 -
    sqlx::query("UPDATE flow_jobs SET placed = ? WHERE id = ?").bind(placed).bind(job_id).execute(pool).await?;
162 +
    sqlx::query("UPDATE flow_jobs SET placed = $1 WHERE id = $2").bind(placed).bind(job_id).execute(pool).await?;
163 163
    Ok(())
164 164
}
165 165
166 166
/// Keep what a job took, as its runner reported.
167 167
pub async fn set_peaks(pool: &Pool, job_id: i64, disk_mib: Option<u64>, memory_mib: Option<u64>) -> sqlx::Result<()> {
168 -
    sqlx::query("UPDATE flow_jobs SET peak_disk_mib = ?, peak_memory_mib = ? WHERE id = ?")
168 +
    sqlx::query("UPDATE flow_jobs SET peak_disk_mib = $1, peak_memory_mib = $2 WHERE id = $3")
169 169
        .bind(disk_mib.map(|v| v.min(i64::MAX as u64) as i64))
170 170
        .bind(memory_mib.map(|v| v.min(i64::MAX as u64) as i64))
171 171
        .bind(job_id)
…
21 unchanged lines
193 193
    let status = if r.error.is_some() { "error" } else { "queued" };
194 194
    let id: i64 = sqlx::query_scalar(
195 195
        "INSERT INTO flow_runs (repo_id, number, bookmark, state, heads, pipeline_hash, status, error, actor, trigger, created_ms, finished_ms) \
196 -
         VALUES (?, (SELECT COALESCE(MAX(number), 0) + 1 FROM flow_runs WHERE repo_id = ?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
196 +
         VALUES ($1, (SELECT COALESCE(MAX(number), 0) + 1 FROM flow_runs WHERE repo_id = $2), $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id",
197 197
    )
198 198
    .bind(r.repo_id)
199 199
    .bind(r.repo_id)
…
12 unchanged lines
212 212
    for j in jobs {
213 213
        sqlx::query(
214 214
            "INSERT INTO flow_jobs (run_id, name, depth, depends, spec, sandbox, runtime, status, need_disk_mib, approval, environment) \
215 -
             VALUES (?, ?, ?, ?, ?, ?, ?, 'queued', ?, ?, ?)",
215 +
             VALUES ($1, $2, $3, $4, $5, $6, $7, 'queued', $8, $9, $10)",
216 216
        )
217 217
        .bind(id)
218 218
        .bind(&j.name)
…
97 unchanged lines
316 316
317 317
/// A repository's runs, newest first, numbered below `before`.
318 318
pub async fn runs_by_number(pool: &Pool, repo_id: i64, limit: i64, before: Option<i64>) -> sqlx::Result<Vec<Run>> {
319 -
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = ? AND number < ? ORDER BY number DESC LIMIT ?")
319 +
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = $1 AND number < $2 ORDER BY number DESC LIMIT $3")
320 320
        .bind(repo_id)
321 321
        .bind(before.unwrap_or(i64::MAX))
322 322
        .bind(limit)
…
8 unchanged lines
331 331
/// that what a cancelled run would have checked is checked by the next.
332 332
pub async fn latest_on(pool: &Pool, repo_id: i64, bookmark: &str) -> sqlx::Result<Option<Run>> {
333 333
    sqlx::query_as(
334 -
        "SELECT * FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND heads != '' AND status != 'cancelled'
334 +
        "SELECT * FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND heads != '' AND status != 'cancelled'
335 335
         AND NOT (trigger = 'push' AND status IN ('queued', 'running')) ORDER BY id DESC LIMIT 1",
336 336
    )
337 337
    .bind(repo_id)
…
3 unchanged lines
341 341
}
342 342
343 343
pub async fn runs(pool: &Pool, repo_id: i64, limit: i64, before: Option<i64>) -> sqlx::Result<Vec<Run>> {
344 -
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = ? AND id < ? ORDER BY id DESC LIMIT ?")
344 +
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = $1 AND id < $2 ORDER BY id DESC LIMIT $3")
345 345
        .bind(repo_id)
346 346
        .bind(before.unwrap_or(i64::MAX))
347 347
        .bind(limit)
…
3 unchanged lines
351 351
352 352
/// A repository's run by its number there.
353 353
pub async fn run(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Option<Run>> {
354 -
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = ? AND number = ?").bind(repo_id).bind(number).fetch_optional(pool).await
354 +
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = $1 AND number = $2").bind(repo_id).bind(number).fetch_optional(pool).await
355 355
}
356 356
357 357
pub async fn run_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<Run>> {
358 -
    sqlx::query_as("SELECT * FROM flow_runs WHERE id = ?").bind(id).fetch_optional(pool).await
358 +
    sqlx::query_as("SELECT * FROM flow_runs WHERE id = $1").bind(id).fetch_optional(pool).await
359 359
}
360 360
361 361
pub async fn jobs(pool: &Pool, run_id: i64) -> sqlx::Result<Vec<Job>> {
362 -
    sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = ? ORDER BY depth, name").bind(run_id).fetch_all(pool).await
362 +
    sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = $1 ORDER BY depth, name").bind(run_id).fetch_all(pool).await
363 363
}
364 364
365 365
pub async fn job(pool: &Pool, id: i64) -> sqlx::Result<Option<Job>> {
366 -
    sqlx::query_as("SELECT * FROM flow_jobs WHERE id = ?").bind(id).fetch_optional(pool).await
366 +
    sqlx::query_as("SELECT * FROM flow_jobs WHERE id = $1").bind(id).fetch_optional(pool).await
367 367
}
368 368
369 369
/// The running job a job token belongs to, while it may still be used.
370 370
pub async fn job_by_token(pool: &Pool, token_hash: &str, max_age_ms: i64) -> sqlx::Result<Option<Job>> {
371 -
    sqlx::query_as("SELECT * FROM flow_jobs WHERE token_hash = ? AND status = 'running' AND started_ms >= ?")
371 +
    sqlx::query_as("SELECT * FROM flow_jobs WHERE token_hash = $1 AND status = 'running' AND started_ms >= $2")
372 372
        .bind(token_hash)
373 373
        .bind(now_ms() - max_age_ms)
374 374
        .fetch_optional(pool)
…
5 unchanged lines
380 380
/// Let a job's token also read these repositories (its pinned subrepos).
381 381
pub async fn set_also_read(pool: &Pool, job_id: i64, repo_ids: &[i64]) -> sqlx::Result<()> {
382 382
    let list: Vec<String> = repo_ids.iter().map(|i| i.to_string()).collect();
383 -
    sqlx::query("UPDATE flow_jobs SET also_read = ? WHERE id = ?").bind(list.join(",")).bind(job_id).execute(pool).await?;
383 +
    sqlx::query("UPDATE flow_jobs SET also_read = $1 WHERE id = $2").bind(list.join(",")).bind(job_id).execute(pool).await?;
384 384
    Ok(())
385 385
}
386 386
…
59 unchanged lines
446 446
        if hash.len() != 64 || !flow_core::arch_ok(arch) {
447 447
            continue;
448 448
        }
449 -
        sqlx::query("INSERT INTO flow_images (hash, arch, seen_ms) VALUES (?, ?, ?) ON CONFLICT (hash) DO UPDATE SET arch = excluded.arch, seen_ms = excluded.seen_ms")
449 +
        sqlx::query("INSERT INTO flow_images (hash, arch, seen_ms) VALUES ($1, $2, $3) ON CONFLICT (hash) DO UPDATE SET arch = excluded.arch, seen_ms = excluded.seen_ms")
450 450
        .bind(hash.to_ascii_lowercase())
451 451
        .bind(arch)
452 452
        .bind(now_ms())
…
9 unchanged lines
462 462
pub async fn learn_archs_from_runs(pool: &Pool) -> sqlx::Result<u64> {
463 463
    let r = sqlx::query(
464 464
        "INSERT INTO flow_images (hash, arch, seen_ms)
465 -
         SELECT lower(json_extract(j.sandbox, '$.rootfs')), MIN(r.arch), ? FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id
465 +
         SELECT lower(json_extract(j.sandbox, '$.rootfs')), MIN(r.arch), $1 FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id
466 466
         WHERE j.status = 'passed' AND r.arch != '' AND length(json_extract(j.sandbox, '$.rootfs')) = 64
467 467
         GROUP BY lower(json_extract(j.sandbox, '$.rootfs'))
468 468
         ON CONFLICT (hash) DO NOTHING",
…
47 unchanged lines
516 516
    let queued: Vec<Job> = sqlx::query_as(
517 517
        "SELECT j.* FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories rep ON rep.id = r.repo_id \
518 518
         WHERE j.status = 'queued' AND j.approval != 'waiting' AND r.status IN ('queued', 'running') \
519 -
         AND (? = '' OR rep.owner = ?) AND (? IS NULL OR r.repo_id = ?) \
520 -
         AND NOT (? = '' AND ? IS NULL AND rep.flow = 'own') \
519 +
         AND ($1 = '' OR rep.owner = $2) AND ($3 IS NULL OR r.repo_id = $4) \
520 +
         AND NOT ($5 = '' AND $6 IS NULL AND rep.flow = 'own') \
521 521
         ORDER BY j.run_id, j.depth, j.id LIMIT 200",
522 522
    )
523 523
    .bind(&scope.owner)
…
21 unchanged lines
545 545
        if !deps.is_empty() {
546 546
            let mut passed = 0i64;
547 547
            for d in &deps {
548 -
                let st: Option<String> = sqlx::query_scalar("SELECT status FROM flow_jobs WHERE run_id = ? AND name = ?")
548 +
                let st: Option<String> = sqlx::query_scalar("SELECT status FROM flow_jobs WHERE run_id = $1 AND name = $2")
549 549
                    .bind(j.run_id)
550 550
                    .bind(d)
551 551
                    .fetch_optional(&mut *tx)
…
11 unchanged lines
563 563
        }
564 564
        let now = now_ms();
565 565
        let done = sqlx::query(
566 -
            "UPDATE flow_jobs SET status = 'running', runner = ?, token_hash = ?, started_ms = ?, waiting = '' WHERE id = ? AND status = 'queued'",
566 +
            "UPDATE flow_jobs SET status = 'running', runner = $1, token_hash = $2, started_ms = $3, waiting = '' WHERE id = $4 AND status = 'queued'",
567 567
        )
568 568
        .bind(runner)
569 569
        .bind(token_hash)
…
2 unchanged lines
572 572
        .execute(&mut *tx)
573 573
        .await?;
574 574
        if done.rows_affected() == 1 {
575 -
            sqlx::query("UPDATE flow_runs SET status = 'running' WHERE id = ? AND status = 'queued'")
575 +
            sqlx::query("UPDATE flow_runs SET status = 'running' WHERE id = $1 AND status = 'queued'")
576 576
                .bind(j.run_id)
577 577
                .execute(&mut *tx)
578 578
                .await?;
…
11 unchanged lines
590 590
591 591
/// The jobs runner `runner_id` declined with this profile, recently.
592 592
pub async fn declined_by(pool: &Pool, runner_id: i64, profile: &str) -> sqlx::Result<std::collections::HashSet<i64>> {
593 -
    let rows: Vec<(i64,)> = sqlx::query_as("SELECT job_id FROM flow_declines WHERE runner_id = ? AND profile = ? AND ms > ?")
593 +
    let rows: Vec<(i64,)> = sqlx::query_as("SELECT job_id FROM flow_declines WHERE runner_id = $1 AND profile = $2 AND ms > $3")
594 594
        .bind(runner_id)
595 595
        .bind(profile)
596 596
        .bind(now_ms() - DECLINE_HOLDS_MS)
…
27 unchanged lines
624 624
    let mut tx = super::begin_write(pool).await?;
625 625
    let back = sqlx::query(
626 626
        "UPDATE flow_jobs SET status = 'queued', runner = '', token_hash = '', started_ms = NULL, runner_id = NULL \
627 -
         WHERE id = ? AND status = 'running' AND runner_id = ?",
627 +
         WHERE id = $1 AND status = 'running' AND runner_id = $2",
628 628
    )
629 629
    .bind(job_id)
630 630
    .bind(runner_id)
…
4 unchanged lines
635 635
        return Ok(None);
636 636
    }
637 637
    sqlx::query(
638 -
        "INSERT INTO flow_declines (job_id, runner_id, runner, profile, reason, ms) VALUES (?, ?, ?, ?, ?, ?) \
638 +
        "INSERT INTO flow_declines (job_id, runner_id, runner, profile, reason, ms) VALUES ($1, $2, $3, $4, $5, $6) \
639 639
         ON CONFLICT (job_id, runner_id) DO UPDATE SET runner = excluded.runner, profile = excluded.profile, reason = excluded.reason, ms = excluded.ms",
640 640
    )
641 641
    .bind(job_id)
…
5 unchanged lines
647 647
    .execute(&mut *tx)
648 648
    .await?;
649 649
    let all: Vec<DeclineRow> =
650 -
        sqlx::query_as("SELECT job_id, runner_id, runner, reason, ms FROM flow_declines WHERE job_id = ? ORDER BY ms")
650 +
        sqlx::query_as("SELECT job_id, runner_id, runner, reason, ms FROM flow_declines WHERE job_id = $1 ORDER BY ms")
651 651
            .bind(job_id)
652 652
            .fetch_all(&mut *tx)
653 653
            .await?;
…
3 unchanged lines
657 657
658 658
/// The declines of job `job_id`, oldest first.
659 659
pub async fn declines_of_job(pool: &Pool, job_id: i64) -> sqlx::Result<Vec<DeclineRow>> {
660 -
    sqlx::query_as("SELECT job_id, runner_id, runner, reason, ms FROM flow_declines WHERE job_id = ? ORDER BY ms")
660 +
    sqlx::query_as("SELECT job_id, runner_id, runner, reason, ms FROM flow_declines WHERE job_id = $1 ORDER BY ms")
661 661
        .bind(job_id)
662 662
        .fetch_all(pool)
663 663
        .await
…
15 unchanged lines
679 679
    sqlx::query_as(
680 680
        "SELECT d.job_id, j.name AS job, r.number AS run_number, rep.owner, rep.name, d.reason, d.ms \
681 681
         FROM flow_declines d JOIN flow_jobs j ON j.id = d.job_id JOIN flow_runs r ON r.id = j.run_id \
682 -
         JOIN repositories rep ON rep.id = r.repo_id WHERE d.runner_id = ? ORDER BY d.ms DESC LIMIT ?",
682 +
         JOIN repositories rep ON rep.id = r.repo_id WHERE d.runner_id = $1 ORDER BY d.ms DESC LIMIT $2",
683 683
    )
684 684
    .bind(runner_id)
685 685
    .bind(limit)
…
14 unchanged lines
700 700
    for j in queued {
701 701
        let mut ready = true;
702 702
        for d in j.depends_on() {
703 -
            let st: Option<String> = sqlx::query_scalar("SELECT status FROM flow_jobs WHERE run_id = ? AND name = ?")
703 +
            let st: Option<String> = sqlx::query_scalar("SELECT status FROM flow_jobs WHERE run_id = $1 AND name = $2")
704 704
                .bind(j.run_id)
705 705
                .bind(&d)
706 706
                .fetch_optional(pool)
…
7 unchanged lines
714 714
            continue;
715 715
        }
716 716
        let repo: Option<(String, i64)> =
717 -
            sqlx::query_as("SELECT rep.owner, rep.id FROM flow_runs r JOIN repositories rep ON rep.id = r.repo_id WHERE r.id = ?")
717 +
            sqlx::query_as("SELECT rep.owner, rep.id FROM flow_runs r JOIN repositories rep ON rep.id = r.repo_id WHERE r.id = $1")
718 718
                .bind(j.run_id)
719 719
                .fetch_optional(pool)
720 720
                .await?;
…
5 unchanged lines
726 726
}
727 727
728 728
pub async fn set_waiting(pool: &Pool, job_id: i64, waiting: &str) -> sqlx::Result<()> {
729 -
    sqlx::query("UPDATE flow_jobs SET waiting = ? WHERE id = ? AND waiting <> ?")
729 +
    sqlx::query("UPDATE flow_jobs SET waiting = $1 WHERE id = $2 AND waiting <> $3")
730 730
        .bind(waiting)
731 731
        .bind(job_id)
732 732
        .bind(waiting)
…
5 unchanged lines
738 738
/// Append records; returns how many were kept (the rest exceed the cap).
739 739
pub async fn append_log(pool: &Pool, job_id: i64, records: &[flow_core::api::Record], cap: i64) -> sqlx::Result<usize> {
740 740
    let mut tx = super::begin_write(pool).await?;
741 -
    let count: i64 = sqlx::query_scalar("SELECT log_count FROM flow_jobs WHERE id = ?").bind(job_id).fetch_one(&mut *tx).await?;
741 +
    let count: i64 = sqlx::query_scalar("SELECT log_count FROM flow_jobs WHERE id = $1").bind(job_id).fetch_one(&mut *tx).await?;
742 742
    let room = (cap - count).max(0) as usize;
743 743
    let keep = &records[..records.len().min(room)];
744 744
    for (i, r) in keep.iter().enumerate() {
745 -
        sqlx::query("INSERT INTO flow_logs (job_id, seq, t, stream, text) VALUES (?, ?, ?, ?, ?)")
745 +
        sqlx::query("INSERT INTO flow_logs (job_id, seq, t, stream, text) VALUES ($1, $2, $3, $4, $5)")
746 746
            .bind(job_id)
747 747
            .bind(count + i as i64 + 1)
748 748
            .bind(r.t as i64)
…
2 unchanged lines
751 751
            .execute(&mut *tx)
752 752
            .await?;
753 753
    }
754 -
    sqlx::query("UPDATE flow_jobs SET log_count = log_count + ? WHERE id = ?")
754 +
    sqlx::query("UPDATE flow_jobs SET log_count = log_count + $1 WHERE id = $2")
755 755
        .bind(keep.len() as i64)
756 756
        .bind(job_id)
757 757
        .execute(&mut *tx)
…
3 unchanged lines
761 761
}
762 762
763 763
pub async fn log(pool: &Pool, job_id: i64, after: i64, limit: i64) -> sqlx::Result<Vec<LogLine>> {
764 -
    sqlx::query_as("SELECT seq, t, stream, text FROM flow_logs WHERE job_id = ? AND seq > ? ORDER BY seq LIMIT ?")
764 +
    sqlx::query_as("SELECT seq, t, stream, text FROM flow_logs WHERE job_id = $1 AND seq > $2 ORDER BY seq LIMIT $3")
765 765
        .bind(job_id)
766 766
        .bind(after)
767 767
        .bind(limit)
…
7 unchanged lines
775 775
pub async fn running_past_limit(pool: &Pool, now: i64, default_s: u64, grace_ms: i64) -> sqlx::Result<Vec<Job>> {
776 776
    sqlx::query_as(
777 777
        "SELECT * FROM flow_jobs WHERE status = 'running' \
778 -
         AND COALESCE(started_ms, 0) + (CASE WHEN timeout_s > 0 THEN timeout_s ELSE ? END) * 1000 + ? < ? ORDER BY started_ms",
778 +
         AND COALESCE(started_ms, 0) + (CASE WHEN timeout_s > 0 THEN timeout_s ELSE $1 END) * 1000 + $2 < $3 ORDER BY started_ms",
779 779
    )
780 780
    .bind(default_s as i64)
781 781
    .bind(grace_ms)
…
4 unchanged lines
786 786
787 787
/// Record the limit a claimed job runs under.
788 788
pub async fn set_job_timeout(pool: &Pool, job_id: i64, timeout_s: u64) -> sqlx::Result<()> {
789 -
    sqlx::query("UPDATE flow_jobs SET timeout_s = ? WHERE id = ?").bind(timeout_s as i64).bind(job_id).execute(pool).await?;
789 +
    sqlx::query("UPDATE flow_jobs SET timeout_s = $1 WHERE id = $2").bind(timeout_s as i64).bind(job_id).execute(pool).await?;
790 790
    Ok(())
791 791
}
792 792
793 793
pub async fn running_since_before(pool: &Pool, ms: i64) -> sqlx::Result<Vec<Job>> {
794 -
    sqlx::query_as("SELECT * FROM flow_jobs WHERE status = 'running' AND COALESCE(started_ms, 0) < ? ORDER BY started_ms")
794 +
    sqlx::query_as("SELECT * FROM flow_jobs WHERE status = 'running' AND COALESCE(started_ms, 0) < $1 ORDER BY started_ms")
795 795
        .bind(ms)
796 796
        .fetch_all(pool)
797 797
        .await
…
1 unchanged lines
799 799
800 800
/// The jobs running on the runner with this id.
801 801
pub async fn running_on(pool: &Pool, runner_id: i64) -> sqlx::Result<Vec<Job>> {
802 -
    sqlx::query_as("SELECT * FROM flow_jobs WHERE status = 'running' AND runner_id = ? ORDER BY id").bind(runner_id).fetch_all(pool).await
802 +
    sqlx::query_as("SELECT * FROM flow_jobs WHERE status = 'running' AND runner_id = $1 ORDER BY id").bind(runner_id).fetch_all(pool).await
803 803
}
804 804
805 805
pub struct Outcome<'a> {
…
9 unchanged lines
815 815
pub async fn finish(pool: &Pool, job_id: i64, o: Outcome<'_>) -> sqlx::Result<Vec<Job>> {
816 816
    let mut tx = super::begin_write(pool).await?;
817 817
    let now = now_ms();
818 -
    let j: Job = sqlx::query_as("SELECT * FROM flow_jobs WHERE id = ?").bind(job_id).fetch_one(&mut *tx).await?;
819 -
    sqlx::query("UPDATE flow_jobs SET status = ?, exit_code = ?, error = ?, receipt = ?, receipt_hash = ?, finished_ms = ?, token_hash = '' WHERE id = ?")
818 +
    let j: Job = sqlx::query_as("SELECT * FROM flow_jobs WHERE id = $1").bind(job_id).fetch_one(&mut *tx).await?;
819 +
    sqlx::query("UPDATE flow_jobs SET status = $1, exit_code = $2, error = $3, receipt = $4, receipt_hash = $5, finished_ms = $6, token_hash = '' WHERE id = $7")
820 820
        .bind(o.status)
821 821
        .bind(o.exit_code)
822 822
        .bind(o.error)
…
12 unchanged lines
835 835
            .unwrap_or_default();
836 836
        if rootfs.len() == 64 {
837 837
            sqlx::query(
838 -
                "INSERT INTO flow_images (hash, arch, seen_ms) SELECT ?, r.arch, ? FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id \
839 -
                 WHERE j.id = ? AND r.arch != '' ON CONFLICT (hash) DO NOTHING",
838 +
                "INSERT INTO flow_images (hash, arch, seen_ms) SELECT $1, r.arch, $2 FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id \
839 +
                 WHERE j.id = $3 AND r.arch != '' ON CONFLICT (hash) DO NOTHING",
840 840
            )
841 841
            .bind(&rootfs)
842 842
            .bind(now)
…
5 unchanged lines
848 848
    let mut changed = vec![job_id];
849 849
    // Dependents of anything that did not pass are skipped, transitively.
850 850
    loop {
851 -
        let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = ?").bind(j.run_id).fetch_all(&mut *tx).await?;
851 +
        let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = $1").bind(j.run_id).fetch_all(&mut *tx).await?;
852 852
        let blocked: Vec<i64> = all
853 853
            .iter()
854 854
            .filter(|x| x.status == "queued")
…
8 unchanged lines
863 863
            break;
864 864
        }
865 865
        for id in blocked {
866 -
            sqlx::query("UPDATE flow_jobs SET status = 'skipped', finished_ms = ? WHERE id = ?")
866 +
            sqlx::query("UPDATE flow_jobs SET status = 'skipped', finished_ms = $1 WHERE id = $2")
867 867
                .bind(now)
868 868
                .bind(id)
869 869
                .execute(&mut *tx)
…
1 unchanged lines
871 871
            changed.push(id);
872 872
        }
873 873
    }
874 -
    let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = ?").bind(j.run_id).fetch_all(&mut *tx).await?;
874 +
    let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = $1").bind(j.run_id).fetch_all(&mut *tx).await?;
875 875
    if all.iter().all(Job::done) {
876 876
        let status = if all.iter().all(|x| x.status == "passed") { "passed" } else { "failed" };
877 -
        sqlx::query("UPDATE flow_runs SET status = ?, finished_ms = ? WHERE id = ?")
877 +
        sqlx::query("UPDATE flow_runs SET status = $1, finished_ms = $2 WHERE id = $3")
878 878
            .bind(status)
879 879
            .bind(now)
880 880
            .bind(j.run_id)
…
16 unchanged lines
897 897
pub async fn cancel_run(pool: &Pool, run_id: i64, why: &str) -> sqlx::Result<Vec<Job>> {
898 898
    let now = now_ms();
899 899
    let mut tx = pool.begin().await?;
900 -
    let ids: Vec<(i64,)> = sqlx::query_as("SELECT id FROM flow_jobs WHERE run_id = ? AND status IN ('queued', 'running')")
900 +
    let ids: Vec<(i64,)> = sqlx::query_as("SELECT id FROM flow_jobs WHERE run_id = $1 AND status IN ('queued', 'running')")
901 901
        .bind(run_id)
902 902
        .fetch_all(&mut *tx)
903 903
        .await?;
904 904
    sqlx::query(
905 -
        "UPDATE flow_jobs SET status = 'cancelled', error = ?, finished_ms = ?, token_hash = ''
906 -
         WHERE run_id = ? AND status IN ('queued', 'running')",
905 +
        "UPDATE flow_jobs SET status = 'cancelled', error = $1, finished_ms = $2, token_hash = ''
906 +
         WHERE run_id = $3 AND status IN ('queued', 'running')",
907 907
    )
908 908
    .bind(why)
909 909
    .bind(now)
910 910
    .bind(run_id)
911 911
    .execute(&mut *tx)
912 912
    .await?;
913 -
    sqlx::query("UPDATE flow_runs SET status = 'cancelled', finished_ms = ? WHERE id = ? AND status IN ('queued', 'running')")
913 +
    sqlx::query("UPDATE flow_runs SET status = 'cancelled', finished_ms = $1 WHERE id = $2 AND status IN ('queued', 'running')")
914 914
        .bind(now)
915 915
        .bind(run_id)
916 916
        .execute(&mut *tx)
…
14 unchanged lines
931 931
/// depend on keep their results. The jobs reset; none when there is no
932 932
/// such job.
933 933
pub async fn rerun_job(pool: &Pool, run_id: i64, name: &str) -> sqlx::Result<Vec<Job>> {
934 -
    let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = ?").bind(run_id).fetch_all(pool).await?;
934 +
    let all: Vec<Job> = sqlx::query_as("SELECT * FROM flow_jobs WHERE run_id = $1").bind(run_id).fetch_all(pool).await?;
935 935
    if !all.iter().any(|j| j.name == name) {
936 936
        return Ok(Vec::new());
937 937
    }
…
13 unchanged lines
951 951
    let mut tx = pool.begin().await?;
952 952
    for id in &ids {
953 953
        for table in ["flow_logs", "flow_artifacts", "flow_declines", "test_reports", "coverage_reports", "reports"] {
954 -
            sqlx::query(&format!("DELETE FROM {table} WHERE job_id = ?")).bind(id).execute(&mut *tx).await?;
954 +
            sqlx::query(&format!("DELETE FROM {table} WHERE job_id = $1")).bind(id).execute(&mut *tx).await?;
955 955
        }
956 956
        sqlx::query(
957 957
            "UPDATE flow_jobs SET status = 'queued', runner = '', token_hash = '', started_ms = NULL, finished_ms = NULL,
958 958
             exit_code = NULL, error = '', receipt = '', receipt_hash = '', log_count = 0, also_read = '', receipt_attest = '',
959 959
             receipt_signer = '', receipt_note = '', runner_id = NULL, waiting = '', placed = '', peak_disk_mib = NULL,
960 960
             peak_memory_mib = NULL, approval = CASE WHEN approval = '' THEN '' ELSE 'waiting' END, approved_by = '',
961 -
             approved_ms = NULL, approval_seal = '' WHERE id = ?",
961 +
             approved_ms = NULL, approval_seal = '' WHERE id = $1",
962 962
        )
963 963
        .bind(id)
964 964
        .execute(&mut *tx)
965 965
        .await?;
966 966
    }
967 -
    sqlx::query("UPDATE flow_runs SET status = 'running', finished_ms = NULL WHERE id = ?").bind(run_id).execute(&mut *tx).await?;
967 +
    sqlx::query("UPDATE flow_runs SET status = 'running', finished_ms = NULL WHERE id = $1").bind(run_id).execute(&mut *tx).await?;
968 968
    tx.commit().await?;
969 969
    let mut out = Vec::new();
970 970
    for id in ids {
…
7 unchanged lines
978 978
/// The push runs of `bookmark` still going, older than run `number`.
979 979
pub async fn live_push_runs_before(pool: &Pool, repo_id: i64, bookmark: &str, number: i64) -> sqlx::Result<Vec<Run>> {
980 980
    sqlx::query_as(
981 -
        "SELECT * FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND number < ? AND trigger = 'push'
981 +
        "SELECT * FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND number < $3 AND trigger = 'push'
982 982
         AND status IN ('queued', 'running') ORDER BY id",
983 983
    )
984 984
    .bind(repo_id)
…
5 unchanged lines
990 990
991 991
/// The newest run of exactly this state (heads joined with `+`, sorted).
992 992
pub async fn latest_for_heads(pool: &Pool, repo_id: i64, heads: &str) -> sqlx::Result<Option<Run>> {
993 -
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = ? AND heads = ? ORDER BY id DESC LIMIT 1")
993 +
    sqlx::query_as("SELECT * FROM flow_runs WHERE repo_id = $1 AND heads = $2 ORDER BY id DESC LIMIT 1")
994 994
        .bind(repo_id)
995 995
        .bind(heads)
996 996
        .fetch_optional(pool)
…
2 unchanged lines
999 999
1000 1000
/// Record what became of job `job_id`'s signed receipt.
1001 1001
pub async fn set_receipt(pool: &Pool, job_id: i64, attest: &str, signer: &str, note: &str) -> sqlx::Result<()> {
1002 -
    sqlx::query("UPDATE flow_jobs SET receipt_attest = ?, receipt_signer = ?, receipt_note = ? WHERE id = ?")
1002 +
    sqlx::query("UPDATE flow_jobs SET receipt_attest = $1, receipt_signer = $2, receipt_note = $3 WHERE id = $4")
1003 1003
        .bind(attest)
1004 1004
        .bind(signer)
1005 1005
        .bind(note)
…
17 unchanged lines
1023 1023
}
1024 1024
1025 1025
pub async fn add_artifact(pool: &Pool, a: &Artifact) -> sqlx::Result<()> {
1026 -
    sqlx::query("INSERT OR REPLACE INTO flow_artifacts (job_id, name, path, blob, size, content, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?)")
1027 -
        .bind(a.job_id)
1028 -
        .bind(&a.name)
1029 -
        .bind(&a.path)
1030 -
        .bind(&a.blob)
1031 -
        .bind(a.size)
1032 -
        .bind(&a.content)
1033 -
        .bind(a.created_ms)
1034 -
        .execute(pool)
1035 -
        .await?;
1026 +
    sqlx::query(
1027 +
        "INSERT OR REPLACE INTO flow_artifacts (job_id, name, path, blob, size, content, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7)",
1028 +
    )
1029 +
    .bind(a.job_id)
1030 +
    .bind(&a.name)
1031 +
    .bind(&a.path)
1032 +
    .bind(&a.blob)
1033 +
    .bind(a.size)
1034 +
    .bind(&a.content)
1035 +
    .bind(a.created_ms)
1036 +
    .execute(pool)
1037 +
    .await?;
1036 1038
    Ok(())
1037 1039
}
1038 1040
…
1 unchanged lines
1040 1042
/// keep too (#126): the database is the only thing that names them.
1041 1043
pub async fn artifact_blobs(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
1042 1044
    sqlx::query_scalar(
1043 -
        "SELECT DISTINCT a.blob FROM flow_artifacts a JOIN flow_jobs j ON j.id = a.job_id JOIN flow_runs r ON r.id = j.run_id WHERE r.repo_id = ?",
1045 +
        "SELECT DISTINCT a.blob FROM flow_artifacts a JOIN flow_jobs j ON j.id = a.job_id JOIN flow_runs r ON r.id = j.run_id WHERE r.repo_id = $1",
1044 1046
    )
1045 1047
    .bind(repo_id)
1046 1048
    .fetch_all(pool)
…
2 unchanged lines
1049 1051
1050 1052
/// Job `job_id`'s artifacts, by name.
1051 1053
pub async fn artifacts(pool: &Pool, job_id: i64) -> sqlx::Result<Vec<Artifact>> {
1052 -
    sqlx::query_as("SELECT * FROM flow_artifacts WHERE job_id = ? ORDER BY name").bind(job_id).fetch_all(pool).await
1054 +
    sqlx::query_as("SELECT * FROM flow_artifacts WHERE job_id = $1 ORDER BY name").bind(job_id).fetch_all(pool).await
1053 1055
}
1054 1056
1055 1057
/// Where an artifact with given content came from: the run and job that
…
14 unchanged lines
1070 1072
    sqlx::query_as(
1071 1073
        "SELECT r.number AS run_number, j.name AS job, a.name AS name, j.receipt_attest, j.receipt_signer \
1072 1074
         FROM flow_artifacts a JOIN flow_jobs j ON j.id = a.job_id JOIN flow_runs r ON r.id = j.run_id \
1073 -
         WHERE r.repo_id = ? AND a.content = ? AND j.status = 'passed' \
1075 +
         WHERE r.repo_id = $1 AND a.content = $2 AND j.status = 'passed' \
1074 1076
         ORDER BY (j.receipt_attest != '') DESC, r.number DESC LIMIT 1",
1075 1077
    )
1076 1078
    .bind(repo_id)
…
6 unchanged lines
1083 1085
/// whether it was.
1084 1086
pub async fn approve_job(pool: &Pool, id: i64, who: &str, seal: &str) -> sqlx::Result<bool> {
1085 1087
    let r = sqlx::query(
1086 -
        "UPDATE flow_jobs SET approval = 'approved', approved_by = ?, approved_ms = ?, approval_seal = ? \
1087 -
         WHERE id = ? AND status = 'queued' AND approval = 'waiting'",
1088 +
        "UPDATE flow_jobs SET approval = 'approved', approved_by = $1, approved_ms = $2, approval_seal = $3 \
1089 +
         WHERE id = $4 AND status = 'queued' AND approval = 'waiting'",
1088 1090
    )
1089 1091
    .bind(who)
1090 1092
    .bind(now_ms())
…
6 unchanged lines
1097 1099
1098 1100
/// Note who rejected job `id` (it is then finished as cancelled).
1099 1101
pub async fn note_rejection(pool: &Pool, id: i64, who: &str) -> sqlx::Result<()> {
1100 -
    sqlx::query("UPDATE flow_jobs SET approved_by = ?, approved_ms = ? WHERE id = ?")
1102 +
    sqlx::query("UPDATE flow_jobs SET approved_by = $1, approved_ms = $2 WHERE id = $3")
1101 1103
        .bind(who)
1102 1104
        .bind(now_ms())
1103 1105
        .bind(id)
…
30 unchanged lines
1134 1136
    sqlx::query_as(
1135 1137
        "SELECT j.environment, j.name AS job, r.number AS run_number, r.state, r.bookmark, r.actor, j.approved_by, \
1136 1138
         COALESCE(j.finished_ms, 0) AS finished_ms FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id \
1137 -
         WHERE r.repo_id = ? AND j.environment != '' AND j.status = 'passed' ORDER BY j.finished_ms DESC LIMIT ?",
1139 +
         WHERE r.repo_id = $1 AND j.environment != '' AND j.status = 'passed' ORDER BY j.finished_ms DESC LIMIT $2",
1138 1140
    )
1139 1141
    .bind(repo_id)
1140 1142
    .bind(limit)
…
3 unchanged lines
modified crates/hub/src/db/hooks.rs +42 −38
…
80 unchanged lines
81 81
}
82 82
83 83
pub async fn hooks_for_repo(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Hook>> {
84 -
    sqlx::query_as("SELECT * FROM hooks WHERE repo_id = ? ORDER BY id").bind(repo_id).fetch_all(pool).await
84 +
    sqlx::query_as("SELECT * FROM hooks WHERE repo_id = $1 ORDER BY id").bind(repo_id).fetch_all(pool).await
85 85
}
86 86
87 87
pub async fn hooks_for_org(pool: &Pool, org_id: i64) -> sqlx::Result<Vec<Hook>> {
88 -
    sqlx::query_as("SELECT * FROM hooks WHERE org_id = ? ORDER BY id").bind(org_id).fetch_all(pool).await
88 +
    sqlx::query_as("SELECT * FROM hooks WHERE org_id = $1 ORDER BY id").bind(org_id).fetch_all(pool).await
89 89
}
90 90
91 91
/// Every hook that should hear about an event in a repository: its own
…
7 unchanged lines
99 99
}
100 100
101 101
pub async fn hook(pool: &Pool, id: i64) -> sqlx::Result<Option<Hook>> {
102 -
    sqlx::query_as("SELECT * FROM hooks WHERE id = ?").bind(id).fetch_optional(pool).await
102 +
    sqlx::query_as("SELECT * FROM hooks WHERE id = $1").bind(id).fetch_optional(pool).await
103 103
}
104 104
105 105
pub async fn create_hook(
…
4 unchanged lines
110 110
    secret: &str,
111 111
    events: &str,
112 112
) -> sqlx::Result<Hook> {
113 -
    sqlx::query_as("INSERT INTO hooks (repo_id, org_id, url, secret, events, created_ms, format) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *")
114 -
        .bind(repo_id)
115 -
        .bind(org_id)
116 -
        .bind(url)
117 -
        .bind(secret)
118 -
        .bind(events)
119 -
        .bind(now_ms())
120 -
        .bind(format_for_url(url))
121 -
        .fetch_one(pool)
122 -
        .await
113 +
    sqlx::query_as(
114 +
        "INSERT INTO hooks (repo_id, org_id, url, secret, events, created_ms, format) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
115 +
    )
116 +
    .bind(repo_id)
117 +
    .bind(org_id)
118 +
    .bind(url)
119 +
    .bind(secret)
120 +
    .bind(events)
121 +
    .bind(now_ms())
122 +
    .bind(format_for_url(url))
123 +
    .fetch_one(pool)
124 +
    .await
123 125
}
124 126
125 127
/// Change a hook; `None` keeps the URL or secret as it is.
…
6 unchanged lines
132 134
    format: &str,
133 135
    active: bool,
134 136
) -> sqlx::Result<()> {
135 -
    sqlx::query("UPDATE hooks SET url = COALESCE(?, url), secret = COALESCE(?, secret), events = ?, format = ?, active = ? WHERE id = ?")
136 -
        .bind(url)
137 -
        .bind(secret)
138 -
        .bind(events)
139 -
        .bind(format)
140 -
        .bind(active)
141 -
        .bind(id)
142 -
        .execute(pool)
143 -
        .await?;
137 +
    sqlx::query(
138 +
        "UPDATE hooks SET url = COALESCE($1, url), secret = COALESCE($2, secret), events = $3, format = $4, active = $5 WHERE id = $6",
139 +
    )
140 +
    .bind(url)
141 +
    .bind(secret)
142 +
    .bind(events)
143 +
    .bind(format)
144 +
    .bind(active)
145 +
    .bind(id)
146 +
    .execute(pool)
147 +
    .await?;
144 148
    Ok(())
145 149
}
146 150
147 151
pub async fn set_hook_format(pool: &Pool, id: i64, format: &str) -> sqlx::Result<()> {
148 -
    sqlx::query("UPDATE hooks SET format = ? WHERE id = ?").bind(format).bind(id).execute(pool).await?;
152 +
    sqlx::query("UPDATE hooks SET format = $1 WHERE id = $2").bind(format).bind(id).execute(pool).await?;
149 153
    Ok(())
150 154
}
151 155
152 156
/// The newest delivery that was attempted, for the settings table.
153 157
pub async fn last_attempt(pool: &Pool, hook_id: i64) -> sqlx::Result<Option<Delivery>> {
154 -
    sqlx::query_as("SELECT * FROM deliveries WHERE hook_id = ? AND attempts > 0 ORDER BY id DESC LIMIT 1")
158 +
    sqlx::query_as("SELECT * FROM deliveries WHERE hook_id = $1 AND attempts > 0 ORDER BY id DESC LIMIT 1")
155 159
        .bind(hook_id)
156 160
        .fetch_optional(pool)
157 161
        .await
158 162
}
159 163
160 164
pub async fn set_hook_active(pool: &Pool, id: i64, active: bool) -> sqlx::Result<()> {
161 -
    sqlx::query("UPDATE hooks SET active = ? WHERE id = ?").bind(active).bind(id).execute(pool).await?;
165 +
    sqlx::query("UPDATE hooks SET active = $1 WHERE id = $2").bind(active).bind(id).execute(pool).await?;
162 166
    Ok(())
163 167
}
164 168
165 169
pub async fn delete_hook(pool: &Pool, id: i64) -> sqlx::Result<()> {
166 -
    sqlx::query("DELETE FROM hooks WHERE id = ?").bind(id).execute(pool).await?;
170 +
    sqlx::query("DELETE FROM hooks WHERE id = $1").bind(id).execute(pool).await?;
167 171
    Ok(())
168 172
}
169 173
…
4 unchanged lines
174 178
/// Queue a delivery; `Ok(None)` when the hook already has
175 179
/// [`MAX_WAITING`] deliveries waiting, so nothing was queued.
176 180
pub async fn queue_delivery(pool: &Pool, hook_id: i64, delivery: &str, event: &str, payload: &str) -> sqlx::Result<Option<i64>> {
177 -
    let waiting: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM deliveries WHERE hook_id = ? AND delivered_ms IS NULL AND next_ms < ?")
181 +
    let waiting: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM deliveries WHERE hook_id = $1 AND delivered_ms IS NULL AND next_ms < $2")
178 182
        .bind(hook_id)
179 183
        .bind(i64::MAX / 4)
180 184
        .fetch_one(pool)
…
3 unchanged lines
184 188
    }
185 189
    let now = now_ms();
186 190
    sqlx::query_scalar(
187 -
        "INSERT INTO deliveries (hook_id, delivery, event, payload, created_ms, next_ms) VALUES (?, ?, ?, ?, ?, ?) RETURNING id",
191 +
        "INSERT INTO deliveries (hook_id, delivery, event, payload, created_ms, next_ms) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
188 192
    )
189 193
    .bind(hook_id)
190 194
    .bind(delivery)
…
7 unchanged lines
198 202
}
199 203
200 204
pub async fn due_deliveries(pool: &Pool, limit: i64) -> sqlx::Result<Vec<Delivery>> {
201 -
    sqlx::query_as("SELECT * FROM deliveries WHERE delivered_ms IS NULL AND next_ms <= ? ORDER BY next_ms LIMIT ?")
205 +
    sqlx::query_as("SELECT * FROM deliveries WHERE delivered_ms IS NULL AND next_ms <= $1 ORDER BY next_ms LIMIT $2")
202 206
        .bind(now_ms())
203 207
        .bind(limit)
204 208
        .fetch_all(pool)
…
1 unchanged lines
206 210
}
207 211
208 212
pub async fn deliveries_for_hook(pool: &Pool, hook_id: i64, limit: i64) -> sqlx::Result<Vec<Delivery>> {
209 -
    sqlx::query_as("SELECT * FROM deliveries WHERE hook_id = ? ORDER BY created_ms DESC, id DESC LIMIT ?")
213 +
    sqlx::query_as("SELECT * FROM deliveries WHERE hook_id = $1 ORDER BY created_ms DESC, id DESC LIMIT $2")
210 214
        .bind(hook_id)
211 215
        .bind(limit)
212 216
        .fetch_all(pool)
…
1 unchanged lines
214 218
}
215 219
216 220
pub async fn delivery(pool: &Pool, id: i64) -> sqlx::Result<Option<Delivery>> {
217 -
    sqlx::query_as("SELECT * FROM deliveries WHERE id = ?").bind(id).fetch_optional(pool).await
221 +
    sqlx::query_as("SELECT * FROM deliveries WHERE id = $1").bind(id).fetch_optional(pool).await
218 222
}
219 223
220 224
/// Record an attempt: done on a 2xx, else retried with backoff up to a day.
…
4 unchanged lines
225 229
    // 1 min, 5 min, 30 min, 2 h, 6 h, then give up after a day's worth.
226 230
    let backoff_ms = [60_000i64, 300_000, 1_800_000, 7_200_000, 21_600_000];
227 231
    let next = if ok || attempts as usize >= backoff_ms.len() { i64::MAX / 4 } else { now + backoff_ms[attempts as usize] };
228 -
    sqlx::query("UPDATE deliveries SET attempts = ?, status = ?, response = ?, delivered_ms = ?, next_ms = ? WHERE id = ?")
232 +
    sqlx::query("UPDATE deliveries SET attempts = $1, status = $2, response = $3, delivered_ms = $4, next_ms = $5 WHERE id = $6")
229 233
        .bind(attempts + 1)
230 234
        .bind(status)
231 235
        .bind(&response.chars().take(2000).collect::<String>())
…
7 unchanged lines
239 243
240 244
/// Requeue a delivery for an immediate retry.
241 245
pub async fn redeliver(pool: &Pool, id: i64) -> sqlx::Result<()> {
242 -
    sqlx::query("UPDATE deliveries SET delivered_ms = NULL, next_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
246 +
    sqlx::query("UPDATE deliveries SET delivered_ms = NULL, next_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
243 247
    Ok(())
244 248
}
245 249
246 250
pub async fn prune_deliveries(pool: &Pool, older_than_ms: i64) -> sqlx::Result<()> {
247 -
    sqlx::query("DELETE FROM deliveries WHERE created_ms < ?").bind(older_than_ms).execute(pool).await?;
251 +
    sqlx::query("DELETE FROM deliveries WHERE created_ms < $1").bind(older_than_ms).execute(pool).await?;
248 252
    Ok(())
249 253
}
250 254
…
22 unchanged lines
273 277
}
274 278
275 279
pub async fn checks_for_state(pool: &Pool, repo_id: i64, state: &str) -> sqlx::Result<Vec<Check>> {
276 -
    sqlx::query_as("SELECT * FROM checks WHERE repo_id = ? AND state = ? ORDER BY name").bind(repo_id).bind(state).fetch_all(pool).await
280 +
    sqlx::query_as("SELECT * FROM checks WHERE repo_id = $1 AND state = $2 ORDER BY name").bind(repo_id).bind(state).fetch_all(pool).await
277 281
}
278 282
279 283
/// Create or update a check by `(state, name)`.
…
11 unchanged lines
291 295
    let now = now_ms();
292 296
    let finished: Option<i64> = if matches!(status, "passed" | "failed" | "cancelled" | "skipped") { Some(now) } else { None };
293 297
    sqlx::query_as(
294 -
        "INSERT INTO checks (repo_id, state, name, status, summary, url, reporter_id, reporter, seal, started_ms, finished_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \
298 +
        "INSERT INTO checks (repo_id, state, name, status, summary, url, reporter_id, reporter, seal, started_ms, finished_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \
295 299
         ON CONFLICT (repo_id, state, name) DO UPDATE SET status = excluded.status, summary = excluded.summary, url = excluded.url, reporter_id = excluded.reporter_id, reporter = excluded.reporter, seal = CASE WHEN excluded.seal = '' THEN checks.seal ELSE excluded.seal END, finished_ms = excluded.finished_ms RETURNING *",
296 300
    )
297 301
    .bind(repo_id)
…
12 unchanged lines
310 314
}
311 315
312 316
pub async fn required_checks(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
313 -
    let s: String = sqlx::query_scalar("SELECT required_checks FROM repositories WHERE id = ?").bind(repo_id).fetch_one(pool).await?;
317 +
    let s: String = sqlx::query_scalar("SELECT required_checks FROM repositories WHERE id = $1").bind(repo_id).fetch_one(pool).await?;
314 318
    Ok(s.split(',').map(str::trim).filter(|x| !x.is_empty()).map(String::from).collect())
315 319
}
316 320
317 321
pub async fn set_required_checks(pool: &Pool, repo_id: i64, names: &[String]) -> sqlx::Result<()> {
318 -
    sqlx::query("UPDATE repositories SET required_checks = ? WHERE id = ?").bind(names.join(",")).bind(repo_id).execute(pool).await?;
322 +
    sqlx::query("UPDATE repositories SET required_checks = $1 WHERE id = $2").bind(names.join(",")).bind(repo_id).execute(pool).await?;
319 323
    Ok(())
320 324
}
modified crates/hub/src/db/issues.rs +51 −50
…
71 unchanged lines
72 72
pub async fn create(pool: &Pool, i: NewIssue<'_>) -> sqlx::Result<IssueRow> {
73 73
    let number = super::proposals::next_number(pool, i.repo_id).await?;
74 74
    let now = now_ms();
75 -
    sqlx::query_as("INSERT INTO issues (repo_id, number, title, body, author_id, author, created_ms, updated_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *")
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 *")
76 76
        .bind(i.repo_id)
77 77
        .bind(number)
78 78
        .bind(i.title)
…
7 unchanged lines
86 86
}
87 87
88 88
pub async fn get(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Option<IssueRow>> {
89 -
    sqlx::query_as("SELECT * FROM issues WHERE repo_id = ? AND number = ?").bind(repo_id).bind(number).fetch_optional(pool).await
89 +
    sqlx::query_as("SELECT * FROM issues WHERE repo_id = $1 AND number = $2").bind(repo_id).bind(number).fetch_optional(pool).await
90 90
}
91 91
92 92
pub async fn get_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<IssueRow>> {
93 -
    sqlx::query_as("SELECT * FROM issues WHERE id = ?").bind(id).fetch_optional(pool).await
93 +
    sqlx::query_as("SELECT * FROM issues WHERE id = $1").bind(id).fetch_optional(pool).await
94 94
}
95 95
96 96
pub async fn list(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<IssueRow>> {
97 -
    sqlx::query_as("SELECT * FROM issues WHERE repo_id = ? ORDER BY pinned DESC, number DESC").bind(repo_id).fetch_all(pool).await
97 +
    sqlx::query_as("SELECT * FROM issues WHERE repo_id = $1 ORDER BY pinned DESC, number DESC").bind(repo_id).fetch_all(pool).await
98 98
}
99 99
100 100
/// Numbers in use by issues and proposals, for cross references.
101 101
pub async fn numbers(pool: &Pool, repo_id: i64) -> sqlx::Result<(Vec<i64>, Vec<i64>)> {
102 -
    let issues: Vec<i64> = sqlx::query_scalar("SELECT number FROM issues WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await?;
103 -
    let proposals: Vec<i64> = sqlx::query_scalar("SELECT number FROM proposals WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await?;
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?;
104 104
    Ok((issues, proposals))
105 105
}
106 106
107 107
pub async fn assigned_to(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<IssueRow>> {
108 -
    sqlx::query_as("SELECT i.* FROM issues i JOIN issue_assignees a ON a.issue_id = i.id WHERE a.account_id = ? AND i.state = 'open' ORDER BY i.updated_ms DESC")
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")
109 109
        .bind(account_id)
110 110
        .fetch_all(pool)
111 111
        .await
112 112
}
113 113
114 114
pub async fn edit(pool: &Pool, id: i64, title: &str, body: &str) -> sqlx::Result<()> {
115 -
    sqlx::query("UPDATE issues SET title = ?, body = ?, updated_ms = ? WHERE id = ?")
115 +
    sqlx::query("UPDATE issues SET title = $1, body = $2, updated_ms = $3 WHERE id = $4")
116 116
        .bind(title)
117 117
        .bind(body)
118 118
        .bind(now_ms())
…
6 unchanged lines
125 125
pub async fn set_state(pool: &Pool, id: i64, state: &str, reason: &str) -> sqlx::Result<()> {
126 126
    let now = now_ms();
127 127
    let closed: Option<i64> = if state == "closed" { Some(now) } else { None };
128 -
    sqlx::query("UPDATE issues SET state = ?, reason = ?, closed_ms = ?, updated_ms = ? WHERE id = ?")
128 +
    sqlx::query("UPDATE issues SET state = $1, reason = $2, closed_ms = $3, updated_ms = $4 WHERE id = $5")
129 129
        .bind(state)
130 130
        .bind(reason)
131 131
        .bind(closed)
…
6 unchanged lines
138 138
139 139
pub async fn set_flags(pool: &Pool, id: i64, locked: Option<bool>, pinned: Option<bool>) -> sqlx::Result<()> {
140 140
    if let Some(l) = locked {
141 -
        sqlx::query("UPDATE issues SET locked = ? WHERE id = ?").bind(l).bind(id).execute(pool).await?;
141 +
        sqlx::query("UPDATE issues SET locked = $1 WHERE id = $2").bind(l).bind(id).execute(pool).await?;
142 142
    }
143 143
    if let Some(p) = pinned {
144 -
        sqlx::query("UPDATE issues SET pinned = ? WHERE id = ?").bind(p).bind(id).execute(pool).await?;
144 +
        sqlx::query("UPDATE issues SET pinned = $1 WHERE id = $2").bind(p).bind(id).execute(pool).await?;
145 145
    }
146 146
    Ok(())
147 147
}
148 148
149 149
pub async fn set_milestone(pool: &Pool, id: i64, milestone_id: Option<i64>) -> sqlx::Result<()> {
150 -
    sqlx::query("UPDATE issues SET milestone_id = ?, updated_ms = ? WHERE id = ?")
150 +
    sqlx::query("UPDATE issues SET milestone_id = $1, updated_ms = $2 WHERE id = $3")
151 151
        .bind(milestone_id)
152 152
        .bind(now_ms())
153 153
        .bind(id)
…
3 unchanged lines
157 157
}
158 158
159 159
pub async fn touch(pool: &Pool, id: i64) -> sqlx::Result<()> {
160 -
    sqlx::query("UPDATE issues SET updated_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
160 +
    sqlx::query("UPDATE issues SET updated_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
161 161
    Ok(())
162 162
}
163 163
164 164
pub async fn comments(pool: &Pool, issue_id: i64) -> sqlx::Result<Vec<IssueComment>> {
165 -
    sqlx::query_as("SELECT * FROM issue_comments WHERE issue_id = ? ORDER BY created_ms, id").bind(issue_id).fetch_all(pool).await
165 +
    sqlx::query_as("SELECT * FROM issue_comments WHERE issue_id = $1 ORDER BY created_ms, id").bind(issue_id).fetch_all(pool).await
166 166
}
167 167
168 168
pub async fn add_comment(pool: &Pool, issue_id: i64, author_id: Option<i64>, author: &str, body: &str) -> sqlx::Result<IssueComment> {
169 -
    let row =
170 -
        sqlx::query_as("INSERT INTO issue_comments (issue_id, author_id, author, body, created_ms) VALUES (?, ?, ?, ?, ?) RETURNING *")
171 -
            .bind(issue_id)
172 -
            .bind(author_id)
173 -
            .bind(author)
174 -
            .bind(body)
175 -
            .bind(now_ms())
176 -
            .fetch_one(pool)
177 -
            .await?;
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?;
178 179
    touch(pool, issue_id).await?;
179 180
    Ok(row)
180 181
}
181 182
182 183
pub async fn edit_comment(pool: &Pool, id: i64, author_id: i64, body: &str) -> sqlx::Result<bool> {
183 -
    let n = sqlx::query("UPDATE issue_comments SET body = ?, edited_ms = ? WHERE id = ? AND author_id = ?")
184 +
    let n = sqlx::query("UPDATE issue_comments SET body = $1, edited_ms = $2 WHERE id = $3 AND author_id = $4")
184 185
        .bind(body)
185 186
        .bind(now_ms())
186 187
        .bind(id)
…
4 unchanged lines
191 192
}
192 193
193 194
pub async fn delete_comment(pool: &Pool, id: i64) -> sqlx::Result<()> {
194 -
    sqlx::query("DELETE FROM issue_comments WHERE id = ?").bind(id).execute(pool).await?;
195 +
    sqlx::query("DELETE FROM issue_comments WHERE id = $1").bind(id).execute(pool).await?;
195 196
    Ok(())
196 197
}
197 198
198 199
pub async fn events(pool: &Pool, issue_id: i64) -> sqlx::Result<Vec<IssueEvent>> {
199 -
    sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM issue_events WHERE issue_id = ? ORDER BY at_ms, id")
200 +
    sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM issue_events WHERE issue_id = $1 ORDER BY at_ms, id")
200 201
        .bind(issue_id)
201 202
        .fetch_all(pool)
202 203
        .await
203 204
}
204 205
205 206
pub async fn add_event(pool: &Pool, issue_id: i64, actor: &str, event: &str, detail: &str) -> sqlx::Result<()> {
206 -
    sqlx::query("INSERT INTO issue_events (issue_id, at_ms, actor, event, detail) VALUES (?, ?, ?, ?, ?)")
207 +
    sqlx::query("INSERT INTO issue_events (issue_id, at_ms, actor, event, detail) VALUES ($1, $2, $3, $4, $5)")
207 208
        .bind(issue_id)
208 209
        .bind(now_ms())
209 210
        .bind(actor)
…
7 unchanged lines
217 218
// ------------------------------------------------------------------ labels
218 219
219 220
pub async fn labels(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Label>> {
220 -
    sqlx::query_as("SELECT * FROM labels WHERE repo_id = ? ORDER BY name").bind(repo_id).fetch_all(pool).await
221 +
    sqlx::query_as("SELECT * FROM labels WHERE repo_id = $1 ORDER BY name").bind(repo_id).fetch_all(pool).await
221 222
}
222 223
223 224
pub async fn label_by_name(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<Option<Label>> {
224 -
    sqlx::query_as("SELECT * FROM labels WHERE repo_id = ? AND name = ?").bind(repo_id).bind(name).fetch_optional(pool).await
225 +
    sqlx::query_as("SELECT * FROM labels WHERE repo_id = $1 AND name = $2").bind(repo_id).bind(name).fetch_optional(pool).await
225 226
}
226 227
227 228
pub async fn upsert_label(pool: &Pool, repo_id: i64, name: &str, colour: &str, description: &str) -> sqlx::Result<Label> {
228 -
    sqlx::query_as("INSERT INTO labels (repo_id, name, colour, description) VALUES (?, ?, ?, ?) ON CONFLICT (repo_id, name) DO UPDATE SET colour = excluded.colour, description = excluded.description RETURNING *")
229 +
    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 *")
229 230
        .bind(repo_id)
230 231
        .bind(name)
231 232
        .bind(colour)
…
3 unchanged lines
235 236
}
236 237
237 238
pub async fn delete_label(pool: &Pool, id: i64) -> sqlx::Result<()> {
238 -
    sqlx::query("DELETE FROM labels WHERE id = ?").bind(id).execute(pool).await?;
239 +
    sqlx::query("DELETE FROM labels WHERE id = $1").bind(id).execute(pool).await?;
239 240
    Ok(())
240 241
}
241 242
242 243
pub async fn issue_labels(pool: &Pool, issue_id: i64) -> sqlx::Result<Vec<Label>> {
243 -
    sqlx::query_as("SELECT l.* FROM labels l JOIN issue_labels il ON il.label_id = l.id WHERE il.issue_id = ? ORDER BY l.name")
244 +
    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")
244 245
        .bind(issue_id)
245 246
        .fetch_all(pool)
246 247
        .await
…
1 unchanged lines
248 249
249 250
pub async fn set_issue_label(pool: &Pool, issue_id: i64, label_id: i64, on: bool) -> sqlx::Result<()> {
250 251
    if on {
251 -
        sqlx::query("INSERT OR IGNORE INTO issue_labels (issue_id, label_id) VALUES (?, ?)")
252 +
        sqlx::query("INSERT OR IGNORE INTO issue_labels (issue_id, label_id) VALUES ($1, $2)")
252 253
            .bind(issue_id)
253 254
            .bind(label_id)
254 255
            .execute(pool)
255 256
            .await?;
256 257
    } else {
257 -
        sqlx::query("DELETE FROM issue_labels WHERE issue_id = ? AND label_id = ?").bind(issue_id).bind(label_id).execute(pool).await?;
258 +
        sqlx::query("DELETE FROM issue_labels WHERE issue_id = $1 AND label_id = $2").bind(issue_id).bind(label_id).execute(pool).await?;
258 259
    }
259 260
    Ok(())
260 261
}
…
2 unchanged lines
263 264
/// Every proposal's labels in a repository, for filtering the list.
264 265
pub async fn labels_by_proposal(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<(i64, Label)>> {
265 266
    let rows: Vec<(i64, i64, i64, String, String, String)> = sqlx::query_as(
266 -
        "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 = ? ORDER BY l.name",
267 +
        "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",
267 268
    )
268 269
    .bind(repo_id)
269 270
    .fetch_all(pool)
…
6 unchanged lines
276 277
277 278
pub async fn labels_by_issue(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<(i64, Label)>> {
278 279
    let rows: Vec<(i64, i64, i64, String, String, String)> = sqlx::query_as(
279 -
        "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 = ? ORDER BY l.name",
280 +
        "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",
280 281
    )
281 282
    .bind(repo_id)
282 283
    .fetch_all(pool)
…
5 unchanged lines
288 289
}
289 290
290 291
pub async fn proposal_labels(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<Label>> {
291 -
    sqlx::query_as("SELECT l.* FROM labels l JOIN proposal_labels pl ON pl.label_id = l.id WHERE pl.proposal_id = ? ORDER BY l.name")
292 +
    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")
292 293
        .bind(proposal_id)
293 294
        .fetch_all(pool)
294 295
        .await
…
1 unchanged lines
296 297
297 298
pub async fn set_proposal_label(pool: &Pool, proposal_id: i64, label_id: i64, on: bool) -> sqlx::Result<()> {
298 299
    if on {
299 -
        sqlx::query("INSERT OR IGNORE INTO proposal_labels (proposal_id, label_id) VALUES (?, ?)")
300 +
        sqlx::query("INSERT OR IGNORE INTO proposal_labels (proposal_id, label_id) VALUES ($1, $2)")
300 301
            .bind(proposal_id)
301 302
            .bind(label_id)
302 303
            .execute(pool)
303 304
            .await?;
304 305
    } else {
305 -
        sqlx::query("DELETE FROM proposal_labels WHERE proposal_id = ? AND label_id = ?")
306 +
        sqlx::query("DELETE FROM proposal_labels WHERE proposal_id = $1 AND label_id = $2")
306 307
            .bind(proposal_id)
307 308
            .bind(label_id)
308 309
            .execute(pool)
…
6 unchanged lines
315 316
316 317
pub async fn assignees(pool: &Pool, issue_id: i64) -> sqlx::Result<Vec<String>> {
317 318
    sqlx::query_scalar(
318 -
        "SELECT a.username FROM accounts a JOIN issue_assignees ia ON ia.account_id = a.id WHERE ia.issue_id = ? ORDER BY a.username",
319 +
        "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",
319 320
    )
320 321
    .bind(issue_id)
321 322
    .fetch_all(pool)
…
2 unchanged lines
324 325
325 326
pub async fn set_assignee(pool: &Pool, issue_id: i64, account_id: i64, on: bool) -> sqlx::Result<()> {
326 327
    if on {
327 -
        sqlx::query("INSERT OR IGNORE INTO issue_assignees (issue_id, account_id) VALUES (?, ?)")
328 +
        sqlx::query("INSERT OR IGNORE INTO issue_assignees (issue_id, account_id) VALUES ($1, $2)")
328 329
            .bind(issue_id)
329 330
            .bind(account_id)
330 331
            .execute(pool)
331 332
            .await?;
332 333
    } else {
333 -
        sqlx::query("DELETE FROM issue_assignees WHERE issue_id = ? AND account_id = ?")
334 +
        sqlx::query("DELETE FROM issue_assignees WHERE issue_id = $1 AND account_id = $2")
334 335
            .bind(issue_id)
335 336
            .bind(account_id)
336 337
            .execute(pool)
…
4 unchanged lines
341 342
342 343
/// Assignees of every issue of a repository, for the list page.
343 344
pub async fn assignees_by_issue(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<(i64, String)>> {
344 -
    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 = ?")
345 +
    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")
345 346
        .bind(repo_id)
346 347
        .fetch_all(pool)
347 348
        .await
…
2 unchanged lines
350 351
// -------------------------------------------------------------- milestones
351 352
352 353
pub async fn milestones(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Milestone>> {
353 -
    sqlx::query_as("SELECT * FROM milestones WHERE repo_id = ? ORDER BY state, due_ms, title").bind(repo_id).fetch_all(pool).await
354 +
    sqlx::query_as("SELECT * FROM milestones WHERE repo_id = $1 ORDER BY state, due_ms, title").bind(repo_id).fetch_all(pool).await
354 355
}
355 356
356 357
pub async fn milestone(pool: &Pool, id: i64) -> sqlx::Result<Option<Milestone>> {
357 -
    sqlx::query_as("SELECT * FROM milestones WHERE id = ?").bind(id).fetch_optional(pool).await
358 +
    sqlx::query_as("SELECT * FROM milestones WHERE id = $1").bind(id).fetch_optional(pool).await
358 359
}
359 360
360 361
pub async fn upsert_milestone(pool: &Pool, repo_id: i64, title: &str, description: &str, due_ms: Option<i64>) -> sqlx::Result<Milestone> {
361 -
    sqlx::query_as("INSERT INTO milestones (repo_id, title, description, due_ms) VALUES (?, ?, ?, ?) ON CONFLICT (repo_id, title) DO UPDATE SET description = excluded.description, due_ms = excluded.due_ms RETURNING *")
362 +
    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 *")
362 363
        .bind(repo_id)
363 364
        .bind(title)
364 365
        .bind(description)
…
4 unchanged lines
369 370
370 371
/// Change a milestone's title, description and due date.
371 372
pub async fn update_milestone(pool: &Pool, id: i64, title: &str, description: &str, due_ms: Option<i64>) -> sqlx::Result<()> {
372 -
    sqlx::query("UPDATE milestones SET title = ?, description = ?, due_ms = ? WHERE id = ?")
373 +
    sqlx::query("UPDATE milestones SET title = $1, description = $2, due_ms = $3 WHERE id = $4")
373 374
        .bind(title)
374 375
        .bind(description)
375 376
        .bind(due_ms)
…
4 unchanged lines
380 381
}
381 382
382 383
pub async fn set_milestone_state(pool: &Pool, id: i64, state: &str) -> sqlx::Result<()> {
383 -
    sqlx::query("UPDATE milestones SET state = ? WHERE id = ?").bind(state).bind(id).execute(pool).await?;
384 +
    sqlx::query("UPDATE milestones SET state = $1 WHERE id = $2").bind(state).bind(id).execute(pool).await?;
384 385
    Ok(())
385 386
}
386 387
387 388
/// `(open, closed)` issue counts per milestone.
388 389
pub async fn milestone_counts(pool: &Pool, milestone_id: i64) -> sqlx::Result<(i64, i64)> {
389 -
    let open: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issues WHERE milestone_id = ? AND state = 'open'")
390 +
    let open: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issues WHERE milestone_id = $1 AND state = 'open'")
390 391
        .bind(milestone_id)
391 392
        .fetch_one(pool)
392 393
        .await?;
393 -
    let closed: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issues WHERE milestone_id = ? AND state = 'closed'")
394 +
    let closed: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issues WHERE milestone_id = $1 AND state = 'closed'")
394 395
        .bind(milestone_id)
395 396
        .fetch_one(pool)
396 397
        .await?;
…
2 unchanged lines
modified crates/hub/src/db/notify.rs +33 −25
…
54 unchanged lines
55 55
pub async fn add(pool: &Pool, n: New<'_>) -> sqlx::Result<()> {
56 56
    let New { account_id, repo_id, reason, kind, title, url, actor } = n;
57 57
    // A new event brings a thread that was set aside back to the inbox.
58 -
    sqlx::query("UPDATE notifications SET done = 0 WHERE account_id = ? AND thread = ? AND done = 1")
58 +
    sqlx::query("UPDATE notifications SET done = 0 WHERE account_id = $1 AND thread = $2 AND done = 1")
59 59
        .bind(account_id)
60 60
        .bind(thread_of(url))
61 61
        .execute(pool)
62 62
        .await?;
63 -
    sqlx::query("INSERT INTO notifications (account_id, at_ms, kind, title, url, actor, repo_id, thread, reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
63 +
    sqlx::query("INSERT INTO notifications (account_id, at_ms, kind, title, url, actor, repo_id, thread, reason) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)")
64 64
        .bind(account_id)
65 65
        .bind(now_ms())
66 66
        .bind(kind)
…
45 unchanged lines
112 112
        "SELECT n.id, MAX(n.at_ms) AS at_ms, n.kind, n.title, n.url, n.actor, n.thread, n.repo_id, \
113 113
         (SELECT r.owner || '/' || r.name FROM repositories r WHERE r.id = n.repo_id) AS repo, \
114 114
         COUNT(*) AS count, SUM(n.read = 0) AS unread, SUM(n.reason = 'participating') > 0 AS participating, n.done \
115 -
         FROM notifications n WHERE n.account_id = ? AND n.done = ?",
115 +
         FROM notifications n WHERE n.account_id = $1 AND n.done = $2",
116 116
    );
117 +
    // Placeholders numbered as they are added: $1 and $2 above.
118 +
    let mut next = 3;
117 119
    if f.kind.is_some() {
118 -
        sql.push_str(" AND (n.kind = ? OR n.kind LIKE ? || '.%')");
120 +
        sql.push_str(&format!(" AND (n.kind = ${} OR n.kind LIKE ${} || '.%')", next, next + 1));
121 +
        next += 2;
119 122
    }
120 123
    if f.repo_id.is_some() {
121 -
        sql.push_str(" AND n.repo_id = ?");
124 +
        sql.push_str(&format!(" AND n.repo_id = ${next}"));
125 +
        next += 1;
122 126
    }
123 127
    sql.push_str(" GROUP BY n.thread HAVING 1");
124 128
    if f.unread_only {
…
4 unchanged lines
129 133
        Some("watching") => sql.push_str(" AND SUM(n.reason = 'participating') = 0"),
130 134
        _ => {}
131 135
    }
132 -
    sql.push_str(" ORDER BY at_ms DESC LIMIT ?");
136 +
    sql.push_str(&format!(" ORDER BY at_ms DESC LIMIT ${next}"));
133 137
    let mut q = sqlx::query_as::<_, Thread>(&sql).bind(account_id).bind(f.done);
134 138
    if let Some(k) = &f.kind {
135 139
        q = q.bind(k).bind(k);
…
8 unchanged lines
144 148
pub async fn set_thread(pool: &Pool, account_id: i64, thread: &str, read: Option<bool>, done: Option<bool>) -> sqlx::Result<()> {
145 149
    if let Some(r) = read {
146 150
        if r {
147 -
            sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ? AND thread = ?")
151 +
            sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = $1 AND thread = $2")
148 152
                .bind(account_id)
149 153
                .bind(thread)
150 154
                .execute(pool)
151 155
                .await?;
152 156
        } else {
153 157
            // Unread: the newest notification of the thread only.
154 -
            sqlx::query("UPDATE notifications SET read = 0 WHERE id = (SELECT id FROM notifications WHERE account_id = ? AND thread = ? ORDER BY at_ms DESC, id DESC LIMIT 1)")
158 +
            sqlx::query("UPDATE notifications SET read = 0 WHERE id = (SELECT id FROM notifications WHERE account_id = $1 AND thread = $2 ORDER BY at_ms DESC, id DESC LIMIT 1)")
155 159
                .bind(account_id)
156 160
                .bind(thread)
157 161
                .execute(pool)
…
2 unchanged lines
160 164
    }
161 165
    if let Some(d) = done {
162 166
        let read_too = if d { ", read = 1" } else { "" };
163 -
        sqlx::query(&format!("UPDATE notifications SET done = ?{read_too} WHERE account_id = ? AND thread = ?"))
167 +
        sqlx::query(&format!("UPDATE notifications SET done = $1{read_too} WHERE account_id = $2 AND thread = $3"))
164 168
            .bind(d)
165 169
            .bind(account_id)
166 170
            .bind(thread)
…
5 unchanged lines
172 176
173 177
/// The notification with this id, if it is the account's.
174 178
pub async fn get(pool: &Pool, account_id: i64, id: i64) -> sqlx::Result<Option<Notification>> {
175 -
    sqlx::query_as("SELECT * FROM notifications WHERE account_id = ? AND id = ?").bind(account_id).bind(id).fetch_optional(pool).await
179 +
    sqlx::query_as("SELECT * FROM notifications WHERE account_id = $1 AND id = $2").bind(account_id).bind(id).fetch_optional(pool).await
176 180
}
177 181
178 182
pub async fn inbox(pool: &Pool, account_id: i64, limit: i64) -> sqlx::Result<Vec<Notification>> {
179 -
    sqlx::query_as("SELECT * FROM notifications WHERE account_id = ? ORDER BY read, at_ms DESC, id DESC LIMIT ?")
183 +
    sqlx::query_as("SELECT * FROM notifications WHERE account_id = $1 ORDER BY read, at_ms DESC, id DESC LIMIT $2")
180 184
        .bind(account_id)
181 185
        .bind(limit)
182 186
        .fetch_all(pool)
…
1 unchanged lines
184 188
}
185 189
186 190
pub async fn unread(pool: &Pool, account_id: i64) -> sqlx::Result<i64> {
187 -
    sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE account_id = ? AND read = 0 AND done = 0")
191 +
    sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE account_id = $1 AND read = 0 AND done = 0")
188 192
        .bind(account_id)
189 193
        .fetch_one(pool)
190 194
        .await
…
2 unchanged lines
193 197
pub async fn mark_read(pool: &Pool, account_id: i64, id: Option<i64>) -> sqlx::Result<()> {
194 198
    match id {
195 199
        Some(id) => {
196 -
            sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ? AND id = ?").bind(account_id).bind(id).execute(pool).await?
200 +
            sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = $1 AND id = $2")
201 +
                .bind(account_id)
202 +
                .bind(id)
203 +
                .execute(pool)
204 +
                .await?
197 205
        }
198 -
        None => sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = ?").bind(account_id).execute(pool).await?,
206 +
        None => sqlx::query("UPDATE notifications SET read = 1 WHERE account_id = $1").bind(account_id).execute(pool).await?,
199 207
    };
200 208
    Ok(())
201 209
}
…
5 unchanged lines
207 215
/// (oxygenhub#183).
208 216
pub async fn owners_of(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<i64>> {
209 217
    let owner: Option<String> =
210 -
        sqlx::query_scalar("SELECT owner FROM repositories WHERE id = ?").bind(repo_id).fetch_optional(pool).await?;
218 +
        sqlx::query_scalar("SELECT owner FROM repositories WHERE id = $1").bind(repo_id).fetch_optional(pool).await?;
211 219
    let Some(owner) = owner else { return Ok(Vec::new()) };
212 220
    if let Some(a) = super::accounts::account_by_name(pool, &owner).await? {
213 221
        return Ok(vec![a.id]);
…
9 unchanged lines
223 231
/// Whether `account_id` watches `repo_id`: a watch it chose, or, for an
224 232
/// owner, unless it muted it.
225 233
pub async fn watching(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
226 -
    let muted: Option<bool> = sqlx::query_scalar("SELECT muted FROM watches WHERE account_id = ? AND repo_id = ?")
234 +
    let muted: Option<bool> = sqlx::query_scalar("SELECT muted FROM watches WHERE account_id = $1 AND repo_id = $2")
227 235
        .bind(account_id)
228 236
        .bind(repo_id)
229 237
        .fetch_optional(pool)
…
6 unchanged lines
236 244
237 245
/// Watch `repo_id`, or stop: for an owner, stopping mutes it.
238 246
pub async fn set_watch(pool: &Pool, account_id: i64, repo_id: i64, on: bool) -> sqlx::Result<()> {
239 -
    sqlx::query("INSERT INTO watches (account_id, repo_id, muted) VALUES (?, ?, ?) ON CONFLICT (account_id, repo_id) DO UPDATE SET muted = excluded.muted")
247 +
    sqlx::query("INSERT INTO watches (account_id, repo_id, muted) VALUES ($1, $2, $3) ON CONFLICT (account_id, repo_id) DO UPDATE SET muted = excluded.muted")
240 248
        .bind(account_id)
241 249
        .bind(repo_id)
242 250
        .bind(!on)
…
6 unchanged lines
249 257
/// have not muted it.
250 258
pub async fn watchers(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<i64>> {
251 259
    let rows: Vec<(i64, bool)> =
252 -
        sqlx::query_as("SELECT account_id, muted FROM watches WHERE repo_id = ?").bind(repo_id).fetch_all(pool).await?;
260 +
        sqlx::query_as("SELECT account_id, muted FROM watches WHERE repo_id = $1").bind(repo_id).fetch_all(pool).await?;
253 261
    let mut out: Vec<i64> = rows.iter().filter(|(_, m)| !m).map(|(a, _)| *a).collect();
254 262
    for o in owners_of(pool, repo_id).await? {
255 263
        if !rows.iter().any(|(a, _)| *a == o) && !out.contains(&o) {
…
4 unchanged lines
260 268
}
261 269
262 270
pub async fn subscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> {
263 -
    sqlx::query("INSERT OR IGNORE INTO subscriptions (account_id, kind, target_id) VALUES (?, ?, ?)")
271 +
    sqlx::query("INSERT OR IGNORE INTO subscriptions (account_id, kind, target_id) VALUES ($1, $2, $3)")
264 272
        .bind(account_id)
265 273
        .bind(kind)
266 274
        .bind(target_id)
…
3 unchanged lines
270 278
}
271 279
272 280
pub async fn unsubscribe(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<()> {
273 -
    sqlx::query("DELETE FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?")
281 +
    sqlx::query("DELETE FROM subscriptions WHERE account_id = $1 AND kind = $2 AND target_id = $3")
274 282
        .bind(account_id)
275 283
        .bind(kind)
276 284
        .bind(target_id)
…
3 unchanged lines
280 288
}
281 289
282 290
pub async fn subscribed(pool: &Pool, account_id: i64, kind: &str, target_id: i64) -> sqlx::Result<bool> {
283 -
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE account_id = ? AND kind = ? AND target_id = ?")
291 +
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM subscriptions WHERE account_id = $1 AND kind = $2 AND target_id = $3")
284 292
        .bind(account_id)
285 293
        .bind(kind)
286 294
        .bind(target_id)
…
3 unchanged lines
290 298
}
291 299
292 300
pub async fn subscribers(pool: &Pool, kind: &str, target_id: i64) -> sqlx::Result<Vec<i64>> {
293 -
    sqlx::query_scalar("SELECT account_id FROM subscriptions WHERE kind = ? AND target_id = ?")
301 +
    sqlx::query_scalar("SELECT account_id FROM subscriptions WHERE kind = $1 AND target_id = $2")
294 302
        .bind(kind)
295 303
        .bind(target_id)
296 304
        .fetch_all(pool)
…
14 unchanged lines
311 319
312 320
/// Replace the indexed text of one thread (title plus body plus comments).
313 321
pub async fn index(pool: &Pool, kind: &str, repo_id: i64, target_id: i64, number: i64, title: &str, body: &str) -> sqlx::Result<()> {
314 -
    sqlx::query("DELETE FROM search WHERE kind = ? AND target_id = ?").bind(kind).bind(target_id).execute(pool).await?;
315 -
    sqlx::query("INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES (?, ?, ?, ?, ?, ?)")
322 +
    sqlx::query("DELETE FROM search WHERE kind = $1 AND target_id = $2").bind(kind).bind(target_id).execute(pool).await?;
323 +
    sqlx::query("INSERT INTO search (kind, repo_id, target_id, number, title, body) VALUES ($1, $2, $3, $4, $5, $6)")
316 324
        .bind(kind)
317 325
        .bind(repo_id)
318 326
        .bind(target_id)
…
13 unchanged lines
332 340
        return Ok(vec![]);
333 341
    }
334 342
    let q = terms.join(" ");
335 -
    let sql = "SELECT kind, repo_id, target_id, number, title, snippet(search, 5, '<mark>', '</mark>', '…', 12) AS snippet FROM search WHERE search MATCH ? AND (? IS NULL OR repo_id = ?) ORDER BY rank LIMIT ?";
343 +
    let sql = "SELECT kind, repo_id, target_id, number, title, snippet(search, 5, '<mark>', '</mark>', '…', 12) AS snippet FROM search WHERE search MATCH $1 AND ($2 IS NULL OR repo_id = $3) ORDER BY rank LIMIT $4";
336 344
    sqlx::query_as(sql).bind(q).bind(repo_id).bind(repo_id).bind(limit).fetch_all(pool).await
337 345
}
modified crates/hub/src/db/projects.rs +35 −35
…
42 unchanged lines
43 43
pub async fn create(pool: &Pool, owner: &str, title: &str, description: &str, public: bool) -> sqlx::Result<Project> {
44 44
    let mut tx = super::begin_write(pool).await?;
45 45
    let number: i64 =
46 -
        sqlx::query_scalar("SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = ?").bind(owner).fetch_one(&mut *tx).await?;
46 +
        sqlx::query_scalar("SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = $1").bind(owner).fetch_one(&mut *tx).await?;
47 47
    let now = now_ms();
48 48
    let p: Project = sqlx::query_as(
49 -
        "INSERT INTO projects (owner, number, title, description, public, created_ms, updated_ms) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *",
49 +
        "INSERT INTO projects (owner, number, title, description, public, created_ms, updated_ms) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
50 50
    )
51 51
    .bind(owner)
52 52
    .bind(number)
…
5 unchanged lines
58 58
    .fetch_one(&mut *tx)
59 59
    .await?;
60 60
    // A board here now: an old address of a transferred one gives way (#125).
61 -
    sqlx::query("DELETE FROM project_redirects WHERE owner = ? AND number = ?").bind(owner).bind(number).execute(&mut *tx).await?;
61 +
    sqlx::query("DELETE FROM project_redirects WHERE owner = $1 AND number = $2").bind(owner).bind(number).execute(&mut *tx).await?;
62 62
    for (i, name) in DEFAULT_COLUMNS.iter().enumerate() {
63 -
        sqlx::query("INSERT INTO project_columns (project_id, name, position) VALUES (?, ?, ?)")
63 +
        sqlx::query("INSERT INTO project_columns (project_id, name, position) VALUES ($1, $2, $3)")
64 64
            .bind(p.id)
65 65
            .bind(name)
66 66
            .bind(i as i64)
…
5 unchanged lines
72 72
}
73 73
74 74
pub async fn of_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<Project>> {
75 -
    sqlx::query_as("SELECT * FROM projects WHERE owner = ? ORDER BY closed, updated_ms DESC").bind(owner).fetch_all(pool).await
75 +
    sqlx::query_as("SELECT * FROM projects WHERE owner = $1 ORDER BY closed, updated_ms DESC").bind(owner).fetch_all(pool).await
76 76
}
77 77
78 78
/// Move project `id` to owner `to` (#125): it takes the next number there,
79 79
/// with its columns, cards and privacy, and its old address redirects to it.
80 80
pub async fn transfer(pool: &Pool, id: i64, to: &str) -> sqlx::Result<Project> {
81 81
    let mut tx = super::begin_write(pool).await?;
82 -
    let old: Project = sqlx::query_as("SELECT * FROM projects WHERE id = ?").bind(id).fetch_one(&mut *tx).await?;
82 +
    let old: Project = sqlx::query_as("SELECT * FROM projects WHERE id = $1").bind(id).fetch_one(&mut *tx).await?;
83 83
    let number: i64 =
84 -
        sqlx::query_scalar("SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = ?").bind(to).fetch_one(&mut *tx).await?;
85 -
    sqlx::query("INSERT OR REPLACE INTO project_redirects (owner, number, project_id) VALUES (?, ?, ?)")
84 +
        sqlx::query_scalar("SELECT COALESCE(MAX(number), 0) + 1 FROM projects WHERE owner = $1").bind(to).fetch_one(&mut *tx).await?;
85 +
    sqlx::query("INSERT OR REPLACE INTO project_redirects (owner, number, project_id) VALUES ($1, $2, $3)")
86 86
        .bind(&old.owner)
87 87
        .bind(old.number)
88 88
        .bind(id)
89 89
        .execute(&mut *tx)
90 90
        .await?;
91 -
    sqlx::query("DELETE FROM project_redirects WHERE owner = ? AND number = ?").bind(to).bind(number).execute(&mut *tx).await?;
92 -
    let moved: Project = sqlx::query_as("UPDATE projects SET owner = ?, number = ?, updated_ms = ? WHERE id = ? RETURNING *")
91 +
    sqlx::query("DELETE FROM project_redirects WHERE owner = $1 AND number = $2").bind(to).bind(number).execute(&mut *tx).await?;
92 +
    let moved: Project = sqlx::query_as("UPDATE projects SET owner = $1, number = $2, updated_ms = $3 WHERE id = $4 RETURNING *")
93 93
        .bind(to)
94 94
        .bind(number)
95 95
        .bind(now_ms())
…
7 unchanged lines
103 103
/// The project an old address (`owner`, `number`) now redirects to, after
104 104
/// a transfer (#125).
105 105
pub async fn redirected(pool: &Pool, owner: &str, number: i64) -> sqlx::Result<Option<Project>> {
106 -
    sqlx::query_as("SELECT p.* FROM project_redirects r JOIN projects p ON p.id = r.project_id WHERE r.owner = ? AND r.number = ?")
106 +
    sqlx::query_as("SELECT p.* FROM project_redirects r JOIN projects p ON p.id = r.project_id WHERE r.owner = $1 AND r.number = $2")
107 107
        .bind(owner)
108 108
        .bind(number)
109 109
        .fetch_optional(pool)
…
1 unchanged lines
111 111
}
112 112
113 113
pub async fn get(pool: &Pool, owner: &str, number: i64) -> sqlx::Result<Option<Project>> {
114 -
    sqlx::query_as("SELECT * FROM projects WHERE owner = ? AND number = ?").bind(owner).bind(number).fetch_optional(pool).await
114 +
    sqlx::query_as("SELECT * FROM projects WHERE owner = $1 AND number = $2").bind(owner).bind(number).fetch_optional(pool).await
115 115
}
116 116
117 117
pub async fn set(pool: &Pool, id: i64, title: &str, description: &str, public: bool, closed: bool) -> sqlx::Result<()> {
118 -
    sqlx::query("UPDATE projects SET title = ?, description = ?, public = ?, closed = ?, updated_ms = ? WHERE id = ?")
118 +
    sqlx::query("UPDATE projects SET title = $1, description = $2, public = $3, closed = $4, updated_ms = $5 WHERE id = $6")
119 119
        .bind(title)
120 120
        .bind(description)
121 121
        .bind(public)
…
6 unchanged lines
128 128
}
129 129
130 130
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
131 -
    sqlx::query("DELETE FROM projects WHERE id = ?").bind(id).execute(pool).await?;
131 +
    sqlx::query("DELETE FROM projects WHERE id = $1").bind(id).execute(pool).await?;
132 132
    Ok(())
133 133
}
134 134
135 135
async fn touch(pool: &Pool, id: i64) -> sqlx::Result<()> {
136 -
    sqlx::query("UPDATE projects SET updated_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
136 +
    sqlx::query("UPDATE projects SET updated_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
137 137
    Ok(())
138 138
}
139 139
140 140
pub async fn columns(pool: &Pool, project_id: i64) -> sqlx::Result<Vec<Column>> {
141 -
    sqlx::query_as("SELECT * FROM project_columns WHERE project_id = ? ORDER BY position, id").bind(project_id).fetch_all(pool).await
141 +
    sqlx::query_as("SELECT * FROM project_columns WHERE project_id = $1 ORDER BY position, id").bind(project_id).fetch_all(pool).await
142 142
}
143 143
144 144
pub async fn cards(pool: &Pool, project_id: i64) -> sqlx::Result<Vec<Card>> {
145 -
    sqlx::query_as("SELECT * FROM project_cards WHERE project_id = ? ORDER BY position, id").bind(project_id).fetch_all(pool).await
145 +
    sqlx::query_as("SELECT * FROM project_cards WHERE project_id = $1 ORDER BY position, id").bind(project_id).fetch_all(pool).await
146 146
}
147 147
148 148
pub async fn add_column(pool: &Pool, project_id: i64, name: &str) -> sqlx::Result<()> {
149 -
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_columns WHERE project_id = ?")
149 +
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_columns WHERE project_id = $1")
150 150
        .bind(project_id)
151 151
        .fetch_one(pool)
152 152
        .await?;
153 -
    sqlx::query("INSERT INTO project_columns (project_id, name, position) VALUES (?, ?, ?)")
153 +
    sqlx::query("INSERT INTO project_columns (project_id, name, position) VALUES ($1, $2, $3)")
154 154
        .bind(project_id)
155 155
        .bind(name)
156 156
        .bind(pos)
…
3 unchanged lines
160 160
}
161 161
162 162
pub async fn rename_column(pool: &Pool, project_id: i64, column_id: i64, name: &str) -> sqlx::Result<()> {
163 -
    sqlx::query("UPDATE project_columns SET name = ? WHERE id = ? AND project_id = ?")
163 +
    sqlx::query("UPDATE project_columns SET name = $1 WHERE id = $2 AND project_id = $3")
164 164
        .bind(name)
165 165
        .bind(column_id)
166 166
        .bind(project_id)
…
13 unchanged lines
180 180
    let mut ids: Vec<i64> = cols.iter().map(|c| c.id).collect();
181 181
    ids.swap(i, j as usize);
182 182
    for (pos, id) in ids.iter().enumerate() {
183 -
        sqlx::query("UPDATE project_columns SET position = ? WHERE id = ?").bind(pos as i64).bind(id).execute(pool).await?;
183 +
        sqlx::query("UPDATE project_columns SET position = $1 WHERE id = $2").bind(pos as i64).bind(id).execute(pool).await?;
184 184
    }
185 185
    touch(pool, project_id).await
186 186
}
…
6 unchanged lines
193 193
        return Ok(false);
194 194
    }
195 195
    let mut tx = super::begin_write(pool).await?;
196 -
    let base: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?")
196 +
    let base: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1")
197 197
        .bind(to.id)
198 198
        .fetch_one(&mut *tx)
199 199
        .await?;
200 -
    sqlx::query("UPDATE project_cards SET column_id = ?, position = position + ? WHERE column_id = ?")
200 +
    sqlx::query("UPDATE project_cards SET column_id = $1, position = position + $2 WHERE column_id = $3")
201 201
        .bind(to.id)
202 202
        .bind(base)
203 203
        .bind(column_id)
204 204
        .execute(&mut *tx)
205 205
        .await?;
206 -
    sqlx::query("DELETE FROM project_columns WHERE id = ?").bind(column_id).execute(&mut *tx).await?;
206 +
    sqlx::query("DELETE FROM project_columns WHERE id = $1").bind(column_id).execute(&mut *tx).await?;
207 207
    tx.commit().await?;
208 208
    touch(pool, project_id).await?;
209 209
    Ok(true)
…
11 unchanged lines
221 221
/// Add a card at the bottom of a column; `Ok(false)` when that issue or
222 222
/// proposal is already on the project.
223 223
pub async fn add_card(pool: &Pool, project_id: i64, c: NewCard<'_>) -> sqlx::Result<bool> {
224 -
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?")
224 +
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1")
225 225
        .bind(c.column_id)
226 226
        .fetch_one(pool)
227 227
        .await?;
228 228
    let r = sqlx::query(
229 -
        "INSERT OR IGNORE INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, added_by, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
229 +
        "INSERT OR IGNORE INTO project_cards (project_id, column_id, position, kind, repo_id, number, note, added_by, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
230 230
    )
231 231
    .bind(project_id)
232 232
    .bind(c.column_id)
…
12 unchanged lines
245 245
246 246
/// Move a card to the bottom of another column of the same project.
247 247
pub async fn move_card(pool: &Pool, project_id: i64, card_id: i64, column_id: i64) -> sqlx::Result<()> {
248 -
    let ok: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM project_columns WHERE id = ? AND project_id = ?")
248 +
    let ok: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM project_columns WHERE id = $1 AND project_id = $2")
249 249
        .bind(column_id)
250 250
        .bind(project_id)
251 251
        .fetch_one(pool)
…
1 unchanged lines
253 253
    if ok == 0 {
254 254
        return Ok(());
255 255
    }
256 -
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = ?")
256 +
    let pos: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(position), -1) + 1 FROM project_cards WHERE column_id = $1")
257 257
        .bind(column_id)
258 258
        .fetch_one(pool)
259 259
        .await?;
260 -
    sqlx::query("UPDATE project_cards SET column_id = ?, position = ? WHERE id = ? AND project_id = ?")
260 +
    sqlx::query("UPDATE project_cards SET column_id = $1, position = $2 WHERE id = $3 AND project_id = $4")
261 261
        .bind(column_id)
262 262
        .bind(pos)
263 263
        .bind(card_id)
…
5 unchanged lines
269 269
270 270
/// Swap a card with its neighbour in its column (`-1` up, `1` down).
271 271
pub async fn shift_card(pool: &Pool, project_id: i64, card_id: i64, by: i64) -> sqlx::Result<()> {
272 -
    let Some(card): Option<Card> = sqlx::query_as("SELECT * FROM project_cards WHERE id = ? AND project_id = ?")
272 +
    let Some(card): Option<Card> = sqlx::query_as("SELECT * FROM project_cards WHERE id = $1 AND project_id = $2")
273 273
        .bind(card_id)
274 274
        .bind(project_id)
275 275
        .fetch_optional(pool)
…
1 unchanged lines
277 277
    else {
278 278
        return Ok(());
279 279
    };
280 -
    let col: Vec<Card> = sqlx::query_as("SELECT * FROM project_cards WHERE column_id = ? ORDER BY position, id")
280 +
    let col: Vec<Card> = sqlx::query_as("SELECT * FROM project_cards WHERE column_id = $1 ORDER BY position, id")
281 281
        .bind(card.column_id)
282 282
        .fetch_all(pool)
283 283
        .await?;
…
5 unchanged lines
289 289
    let mut ids: Vec<i64> = col.iter().map(|c| c.id).collect();
290 290
    ids.swap(i, j as usize);
291 291
    for (pos, id) in ids.iter().enumerate() {
292 -
        sqlx::query("UPDATE project_cards SET position = ? WHERE id = ?").bind(pos as i64).bind(id).execute(pool).await?;
292 +
        sqlx::query("UPDATE project_cards SET position = $1 WHERE id = $2").bind(pos as i64).bind(id).execute(pool).await?;
293 293
    }
294 294
    touch(pool, project_id).await
295 295
}
296 296
297 297
pub async fn remove_card(pool: &Pool, project_id: i64, card_id: i64) -> sqlx::Result<()> {
298 -
    sqlx::query("DELETE FROM project_cards WHERE id = ? AND project_id = ?").bind(card_id).bind(project_id).execute(pool).await?;
298 +
    sqlx::query("DELETE FROM project_cards WHERE id = $1 AND project_id = $2").bind(card_id).bind(project_id).execute(pool).await?;
299 299
    touch(pool, project_id).await
300 300
}
301 301
302 302
/// The projects (and column names) an issue or proposal is on.
303 303
pub async fn placements(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Vec<(Project, String)>> {
304 304
    let rows: Vec<(i64, String)> = sqlx::query_as(
305 -
        "SELECT c.project_id, col.name FROM project_cards c JOIN project_columns col ON col.id = c.column_id WHERE c.repo_id = ? AND c.number = ? AND c.kind <> 'note'",
305 +
        "SELECT c.project_id, col.name FROM project_cards c JOIN project_columns col ON col.id = c.column_id WHERE c.repo_id = $1 AND c.number = $2 AND c.kind <> 'note'",
306 306
    )
307 307
    .bind(repo_id)
308 308
    .bind(number)
…
1 unchanged lines
310 310
    .await?;
311 311
    let mut out = Vec::new();
312 312
    for (pid, col) in rows {
313 -
        if let Some(p) = sqlx::query_as::<_, Project>("SELECT * FROM projects WHERE id = ?").bind(pid).fetch_optional(pool).await? {
313 +
        if let Some(p) = sqlx::query_as::<_, Project>("SELECT * FROM projects WHERE id = $1").bind(pid).fetch_optional(pool).await? {
314 314
            out.push((p, col));
315 315
        }
316 316
    }
…
2 unchanged lines
modified crates/hub/src/db/proposals.rs +25 −25
…
95 unchanged lines
96 96
/// Take the next number of a repository's shared number space.
97 97
pub async fn next_number(pool: &Pool, repo_id: i64) -> sqlx::Result<i64> {
98 98
    let mut tx = super::begin_write(pool).await?;
99 -
    sqlx::query("INSERT OR IGNORE INTO numbers (repo_id, next) VALUES (?, 1)").bind(repo_id).execute(&mut *tx).await?;
100 -
    let n: i64 = sqlx::query_scalar("SELECT next FROM numbers WHERE repo_id = ?").bind(repo_id).fetch_one(&mut *tx).await?;
101 -
    sqlx::query("UPDATE numbers SET next = next + 1 WHERE repo_id = ?").bind(repo_id).execute(&mut *tx).await?;
99 +
    sqlx::query("INSERT OR IGNORE INTO numbers (repo_id, next) VALUES ($1, 1)").bind(repo_id).execute(&mut *tx).await?;
100 +
    let n: i64 = sqlx::query_scalar("SELECT next FROM numbers WHERE repo_id = $1").bind(repo_id).fetch_one(&mut *tx).await?;
101 +
    sqlx::query("UPDATE numbers SET next = next + 1 WHERE repo_id = $1").bind(repo_id).execute(&mut *tx).await?;
102 102
    tx.commit().await?;
103 103
    Ok(n)
104 104
}
…
17 unchanged lines
122 122
    let now = now_ms();
123 123
    sqlx::query_as(
124 124
        "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) \
125 -
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING *",
125 +
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *",
126 126
    )
127 127
    .bind(p.repo_id)
128 128
    .bind(number)
…
14 unchanged lines
143 143
}
144 144
145 145
pub async fn get(pool: &Pool, repo_id: i64, number: i64) -> sqlx::Result<Option<ProposalRow>> {
146 -
    sqlx::query_as("SELECT * FROM proposals WHERE repo_id = ? AND number = ?").bind(repo_id).bind(number).fetch_optional(pool).await
146 +
    sqlx::query_as("SELECT * FROM proposals WHERE repo_id = $1 AND number = $2").bind(repo_id).bind(number).fetch_optional(pool).await
147 147
}
148 148
149 149
pub async fn list(pool: &Pool, repo_id: i64, states: &[&str]) -> sqlx::Result<Vec<ProposalRow>> {
150 150
    let all: Vec<ProposalRow> =
151 -
        sqlx::query_as("SELECT * FROM proposals WHERE repo_id = ? ORDER BY number DESC").bind(repo_id).fetch_all(pool).await?;
151 +
        sqlx::query_as("SELECT * FROM proposals WHERE repo_id = $1 ORDER BY number DESC").bind(repo_id).fetch_all(pool).await?;
152 152
    Ok(all.into_iter().filter(|p| states.is_empty() || states.contains(&p.state.as_str())).collect())
153 153
}
154 154
155 155
/// Open proposals of every repository, for the front page.
156 156
pub async fn open_for_author(pool: &Pool, author_id: i64) -> sqlx::Result<Vec<ProposalRow>> {
157 -
    sqlx::query_as("SELECT * FROM proposals WHERE author_id = ? AND state IN ('open', 'draft') ORDER BY updated_ms DESC")
157 +
    sqlx::query_as("SELECT * FROM proposals WHERE author_id = $1 AND state IN ('open', 'draft') ORDER BY updated_ms DESC")
158 158
        .bind(author_id)
159 159
        .fetch_all(pool)
160 160
        .await
…
8 unchanged lines
169 169
    changes: &str,
170 170
    conflicted: bool,
171 171
) -> sqlx::Result<()> {
172 -
    sqlx::query("UPDATE proposals SET projected_state = ?, projected_heads = ?, projected_target = ?, projected_changes = ?, conflicted = ? WHERE id = ?")
172 +
    sqlx::query("UPDATE proposals SET projected_state = $1, projected_heads = $2, projected_target = $3, projected_changes = $4, conflicted = $5 WHERE id = $6")
173 173
        .bind(state)
174 174
        .bind(heads)
175 175
        .bind(target_state)
…
6 unchanged lines
182 182
}
183 183
184 184
pub async fn set_state(pool: &Pool, id: i64, state: &str, landed_state: &str) -> sqlx::Result<()> {
185 -
    sqlx::query("UPDATE proposals SET state = ?, landed_state = ?, updated_ms = ? WHERE id = ?")
185 +
    sqlx::query("UPDATE proposals SET state = $1, landed_state = $2, updated_ms = $3 WHERE id = $4")
186 186
        .bind(state)
187 187
        .bind(landed_state)
188 188
        .bind(now_ms())
…
7 unchanged lines
196 196
pub async fn set_landed(pool: &Pool, id: i64, landed_state: &str, by: &str, via: &str) -> sqlx::Result<()> {
197 197
    let now = now_ms();
198 198
    sqlx::query(
199 -
        "UPDATE proposals SET state = 'landed', landed_state = ?, landed_by = ?, landed_via = ?, landed_ms = ?, updated_ms = ? WHERE id = ?",
199 +
        "UPDATE proposals SET state = 'landed', landed_state = $1, landed_by = $2, landed_via = $3, landed_ms = $4, updated_ms = $5 WHERE id = $6",
200 200
    )
201 201
    .bind(landed_state)
202 202
    .bind(by)
…
7 unchanged lines
210 210
}
211 211
212 212
pub async fn edit(pool: &Pool, id: i64, title: &str, body: &str, target: &str) -> sqlx::Result<()> {
213 -
    sqlx::query("UPDATE proposals SET title = ?, body = ?, target = ?, updated_ms = ? WHERE id = ?")
213 +
    sqlx::query("UPDATE proposals SET title = $1, body = $2, target = $3, updated_ms = $4 WHERE id = $5")
214 214
        .bind(title)
215 215
        .bind(body)
216 216
        .bind(target)
…
5 unchanged lines
222 222
}
223 223
224 224
pub async fn touch(pool: &Pool, id: i64) -> sqlx::Result<()> {
225 -
    sqlx::query("UPDATE proposals SET updated_ms = ? WHERE id = ?").bind(now_ms()).bind(id).execute(pool).await?;
225 +
    sqlx::query("UPDATE proposals SET updated_ms = $1 WHERE id = $2").bind(now_ms()).bind(id).execute(pool).await?;
226 226
    Ok(())
227 227
}
228 228
229 229
pub async fn comments(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<CommentRow>> {
230 -
    sqlx::query_as("SELECT * FROM proposal_comments WHERE proposal_id = ? ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
230 +
    sqlx::query_as("SELECT * FROM proposal_comments WHERE proposal_id = $1 ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
231 231
}
232 232
233 233
pub async fn add_comment(
…
6 unchanged lines
240 240
    anchor: Option<(&str, i64)>,
241 241
) -> sqlx::Result<CommentRow> {
242 242
    let row = sqlx::query_as(
243 -
        "INSERT INTO proposal_comments (proposal_id, author_id, author, body, path, anchor_change, anchor_index, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *",
243 +
        "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 *",
244 244
    )
245 245
    .bind(proposal_id)
246 246
    .bind(author_id)
…
20 unchanged lines
267 267
    via: &str,
268 268
) -> sqlx::Result<CommentRow> {
269 269
    let row = sqlx::query_as(
270 -
        "INSERT INTO proposal_comments (proposal_id, author_id, author, body, path, anchor_change, anchor_index, created_ms, via) VALUES (?, ?, ?, ?, '', '', 0, ?, ?) RETURNING *",
270 +
        "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 *",
271 271
    )
272 272
    .bind(proposal_id)
273 273
    .bind(author_id)
…
8 unchanged lines
282 282
}
283 283
284 284
pub async fn set_comment_resolved(pool: &Pool, id: i64, resolved: bool) -> sqlx::Result<()> {
285 -
    sqlx::query("UPDATE proposal_comments SET resolved = ? WHERE id = ?").bind(resolved).bind(id).execute(pool).await?;
285 +
    sqlx::query("UPDATE proposal_comments SET resolved = $1 WHERE id = $2").bind(resolved).bind(id).execute(pool).await?;
286 286
    Ok(())
287 287
}
288 288
289 289
pub async fn reviews(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<ReviewRow>> {
290 -
    sqlx::query_as("SELECT * FROM proposal_reviews WHERE proposal_id = ? ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
290 +
    sqlx::query_as("SELECT * FROM proposal_reviews WHERE proposal_id = $1 ORDER BY created_ms, id").bind(proposal_id).fetch_all(pool).await
291 291
}
292 292
293 293
pub async fn add_review(pool: &Pool, proposal_id: i64, author_id: i64, author: &str, verdict: &str, body: &str) -> sqlx::Result<ReviewRow> {
…
11 unchanged lines
305 305
    via: &str,
306 306
) -> sqlx::Result<ReviewRow> {
307 307
    let row = sqlx::query_as(
308 -
        "INSERT INTO proposal_reviews (proposal_id, author_id, author, verdict, body, created_ms, via) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *",
308 +
        "INSERT INTO proposal_reviews (proposal_id, author_id, author, verdict, body, created_ms, via) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *",
309 309
    )
310 310
    .bind(proposal_id)
311 311
    .bind(author_id)
…
9 unchanged lines
321 321
}
322 322
323 323
pub async fn events(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<EventRow>> {
324 -
    sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM proposal_events WHERE proposal_id = ? ORDER BY at_ms, id")
324 +
    sqlx::query_as("SELECT id, at_ms, actor, event, detail FROM proposal_events WHERE proposal_id = $1 ORDER BY at_ms, id")
325 325
        .bind(proposal_id)
326 326
        .fetch_all(pool)
327 327
        .await
328 328
}
329 329
330 330
pub async fn add_event(pool: &Pool, proposal_id: i64, actor: &str, event: &str, detail: &str) -> sqlx::Result<()> {
331 -
    sqlx::query("INSERT INTO proposal_events (proposal_id, at_ms, actor, event, detail) VALUES (?, ?, ?, ?, ?)")
331 +
    sqlx::query("INSERT INTO proposal_events (proposal_id, at_ms, actor, event, detail) VALUES ($1, $2, $3, $4, $5)")
332 332
        .bind(proposal_id)
333 333
        .bind(now_ms())
334 334
        .bind(actor)
…
13 unchanged lines
348 348
}
349 349
350 350
pub async fn requirements(pool: &Pool, repo_id: i64) -> sqlx::Result<Requirements> {
351 -
    sqlx::query_as("SELECT required_approvals, require_owners, allow_conflicts FROM repositories WHERE id = ?")
351 +
    sqlx::query_as("SELECT required_approvals, require_owners, allow_conflicts FROM repositories WHERE id = $1")
352 352
        .bind(repo_id)
353 353
        .fetch_one(pool)
354 354
        .await
355 355
}
356 356
357 357
pub async fn set_requirements(pool: &Pool, repo_id: i64, r: Requirements) -> sqlx::Result<()> {
358 -
    sqlx::query("UPDATE repositories SET required_approvals = ?, require_owners = ?, allow_conflicts = ? WHERE id = ?")
358 +
    sqlx::query("UPDATE repositories SET required_approvals = $1, require_owners = $2, allow_conflicts = $3 WHERE id = $4")
359 359
        .bind(r.required_approvals)
360 360
        .bind(r.require_owners)
361 361
        .bind(r.allow_conflicts)
…
8 unchanged lines
370 370
/// Ask an account to review a proposal; false when it was already asked.
371 371
pub async fn request_review(pool: &Pool, proposal_id: i64, account_id: i64, by: &str) -> sqlx::Result<bool> {
372 372
    let r = sqlx::query(
373 -
        "INSERT OR IGNORE INTO proposal_review_requests (proposal_id, account_id, requested_by, requested_ms) VALUES (?, ?, ?, ?)",
373 +
        "INSERT OR IGNORE INTO proposal_review_requests (proposal_id, account_id, requested_by, requested_ms) VALUES ($1, $2, $3, $4)",
374 374
    )
375 375
    .bind(proposal_id)
376 376
    .bind(account_id)
…
6 unchanged lines
383 383
384 384
/// Withdraw a request, or mark it answered; false when there was none.
385 385
pub async fn unrequest_review(pool: &Pool, proposal_id: i64, account_id: i64) -> sqlx::Result<bool> {
386 -
    let r = sqlx::query("DELETE FROM proposal_review_requests WHERE proposal_id = ? AND account_id = ?")
386 +
    let r = sqlx::query("DELETE FROM proposal_review_requests WHERE proposal_id = $1 AND account_id = $2")
387 387
        .bind(proposal_id)
388 388
        .bind(account_id)
389 389
        .execute(pool)
…
6 unchanged lines
396 396
pub async fn review_requests(pool: &Pool, proposal_id: i64) -> sqlx::Result<Vec<String>> {
397 397
    sqlx::query_scalar(
398 398
        "SELECT a.username FROM proposal_review_requests r JOIN accounts a ON a.id = r.account_id \
399 -
         WHERE r.proposal_id = ? ORDER BY r.requested_ms, a.username",
399 +
         WHERE r.proposal_id = $1 ORDER BY r.requested_ms, a.username",
400 400
    )
401 401
    .bind(proposal_id)
402 402
    .fetch_all(pool)
…
2 unchanged lines
modified crates/hub/src/db/releases.rs +16 −12
…
28 unchanged lines
29 29
}
30 30
31 31
pub async fn list(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<Release>> {
32 -
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = ? ORDER BY created_ms DESC, id DESC").bind(repo_id).fetch_all(pool).await
32 +
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 ORDER BY created_ms DESC, id DESC").bind(repo_id).fetch_all(pool).await
33 33
}
34 34
35 35
pub async fn get(pool: &Pool, repo_id: i64, tag: &str) -> sqlx::Result<Option<Release>> {
36 -
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = ? AND tag = ?").bind(repo_id).bind(tag).fetch_optional(pool).await
36 +
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 AND tag = $2").bind(repo_id).bind(tag).fetch_optional(pool).await
37 37
}
38 38
39 39
/// The newest release that is not a pre-release: the "latest" marker.
40 40
pub async fn latest(pool: &Pool, repo_id: i64) -> sqlx::Result<Option<Release>> {
41 -
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = ? AND prerelease = 0 ORDER BY created_ms DESC, id DESC LIMIT 1")
41 +
    sqlx::query_as("SELECT * FROM releases WHERE repo_id = $1 AND prerelease = 0 ORDER BY created_ms DESC, id DESC LIMIT 1")
42 42
        .bind(repo_id)
43 43
        .fetch_optional(pool)
44 44
        .await
…
8 unchanged lines
53 53
    prerelease: bool,
54 54
    author: Option<(i64, &str)>,
55 55
) -> sqlx::Result<Release> {
56 -
    sqlx::query_as("INSERT INTO releases (repo_id, tag, title, notes, prerelease, author_id, author, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *")
56 +
    sqlx::query_as("INSERT INTO releases (repo_id, tag, title, notes, prerelease, author_id, author, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *")
57 57
        .bind(repo_id)
58 58
        .bind(tag)
59 59
        .bind(title)
…
7 unchanged lines
67 67
}
68 68
69 69
pub async fn edit(pool: &Pool, id: i64, title: &str, notes: &str, prerelease: bool) -> sqlx::Result<()> {
70 -
    sqlx::query("UPDATE releases SET title = ?, notes = ?, prerelease = ? WHERE id = ?")
70 +
    sqlx::query("UPDATE releases SET title = $1, notes = $2, prerelease = $3 WHERE id = $4")
71 71
        .bind(title)
72 72
        .bind(notes)
73 73
        .bind(prerelease)
…
4 unchanged lines
78 78
}
79 79
80 80
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
81 -
    sqlx::query("DELETE FROM releases WHERE id = ?").bind(id).execute(pool).await?;
81 +
    sqlx::query("DELETE FROM releases WHERE id = $1").bind(id).execute(pool).await?;
82 82
    Ok(())
83 83
}
84 84
85 85
pub async fn assets(pool: &Pool, release_id: i64) -> sqlx::Result<Vec<Asset>> {
86 -
    sqlx::query_as("SELECT * FROM release_assets WHERE release_id = ? ORDER BY name").bind(release_id).fetch_all(pool).await
86 +
    sqlx::query_as("SELECT * FROM release_assets WHERE release_id = $1 ORDER BY name").bind(release_id).fetch_all(pool).await
87 87
}
88 88
89 89
pub async fn asset(pool: &Pool, release_id: i64, name: &str) -> sqlx::Result<Option<Asset>> {
90 -
    sqlx::query_as("SELECT * FROM release_assets WHERE release_id = ? AND name = ?").bind(release_id).bind(name).fetch_optional(pool).await
90 +
    sqlx::query_as("SELECT * FROM release_assets WHERE release_id = $1 AND name = $2")
91 +
        .bind(release_id)
92 +
        .bind(name)
93 +
        .fetch_optional(pool)
94 +
        .await
91 95
}
92 96
93 97
pub async fn add_asset(pool: &Pool, release_id: i64, name: &str, size: i64, hash: &str, content_type: &str) -> sqlx::Result<Asset> {
94 98
    sqlx::query_as(
95 -
        "INSERT INTO release_assets (release_id, name, size, hash, content_type, created_ms) VALUES (?, ?, ?, ?, ?, ?) \
99 +
        "INSERT INTO release_assets (release_id, name, size, hash, content_type, created_ms) VALUES ($1, $2, $3, $4, $5, $6) \
96 100
         ON CONFLICT (release_id, name) DO UPDATE SET size = excluded.size, hash = excluded.hash, content_type = excluded.content_type, created_ms = excluded.created_ms RETURNING *",
97 101
    )
98 102
    .bind(release_id)
…
7 unchanged lines
106 110
}
107 111
108 112
pub async fn delete_asset(pool: &Pool, id: i64) -> sqlx::Result<()> {
109 -
    sqlx::query("DELETE FROM release_assets WHERE id = ?").bind(id).execute(pool).await?;
113 +
    sqlx::query("DELETE FROM release_assets WHERE id = $1").bind(id).execute(pool).await?;
110 114
    Ok(())
111 115
}
112 116
113 117
pub async fn count_download(pool: &Pool, id: i64) -> sqlx::Result<()> {
114 -
    sqlx::query("UPDATE release_assets SET downloads = downloads + 1 WHERE id = ?").bind(id).execute(pool).await?;
118 +
    sqlx::query("UPDATE release_assets SET downloads = downloads + 1 WHERE id = $1").bind(id).execute(pool).await?;
115 119
    Ok(())
116 120
}
117 121
118 122
/// Whether any asset of any release still names this content.
119 123
pub async fn hash_in_use(pool: &Pool, hash: &str) -> sqlx::Result<bool> {
120 -
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM release_assets WHERE hash = ?").bind(hash).fetch_one(pool).await?;
124 +
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM release_assets WHERE hash = $1").bind(hash).fetch_one(pool).await?;
121 125
    Ok(n > 0)
122 126
}
modified crates/hub/src/db/repos.rs +29 −29
…
95 unchanged lines
96 96
}
97 97
98 98
pub async fn get_by_id(pool: &Pool, id: i64) -> sqlx::Result<Option<RepoRow>> {
99 -
    sqlx::query_as("SELECT * FROM repositories WHERE id = ?").bind(id).fetch_optional(pool).await
99 +
    sqlx::query_as("SELECT * FROM repositories WHERE id = $1").bind(id).fetch_optional(pool).await
100 100
}
101 101
102 102
pub async fn list_by_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<RepoRow>> {
103 -
    sqlx::query_as("SELECT * FROM repositories WHERE owner = ? ORDER BY updated_ms DESC, name").bind(owner).fetch_all(pool).await
103 +
    sqlx::query_as("SELECT * FROM repositories WHERE owner = $1 ORDER BY updated_ms DESC, name").bind(owner).fetch_all(pool).await
104 104
}
105 105
106 106
/// One repository, with its open issue and proposal counts for the tabs.
…
2 unchanged lines
109 109
        "SELECT r.*, \
110 110
           (SELECT COUNT(*) FROM issues i WHERE i.repo_id = r.id AND i.state = 'open') AS open_issues, \
111 111
           (SELECT COUNT(*) FROM proposals p WHERE p.repo_id = r.id AND p.state IN ('open', 'draft')) AS open_proposals \
112 -
         FROM repositories r WHERE r.owner = ? AND r.name = ?",
112 +
         FROM repositories r WHERE r.owner = $1 AND r.name = $2",
113 113
    )
114 114
    .bind(owner)
115 115
    .bind(name)
…
8 unchanged lines
124 124
    let now = now_ms();
125 125
    sqlx::query_as(
126 126
        "INSERT INTO repositories (owner, name, description, visibility, created_ms, updated_ms, required_approvals) \
127 -
         VALUES (?, ?, ?, ?, ?, ?, (SELECT COUNT(*) >= 2 FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = ?)) RETURNING *",
127 +
         VALUES ($1, $2, $3, $4, $5, $6, (SELECT COUNT(*) >= 2 FROM org_members m JOIN organisations o ON o.id = m.org_id WHERE o.name = $7)) RETURNING *",
128 128
    )
129 129
    .bind(owner)
130 130
    .bind(name)
…
8 unchanged lines
139 139
140 140
/// The repository an old name now points at, after a transfer.
141 141
pub async fn moved_to(pool: &Pool, owner: &str, name: &str) -> sqlx::Result<Option<RepoRow>> {
142 -
    let id: Option<i64> = sqlx::query_scalar("SELECT repo_id FROM repo_redirects WHERE old_owner = ? AND old_name = ?")
142 +
    let id: Option<i64> = sqlx::query_scalar("SELECT repo_id FROM repo_redirects WHERE old_owner = $1 AND old_name = $2")
143 143
        .bind(owner)
144 144
        .bind(name)
145 145
        .fetch_optional(pool)
…
8 unchanged lines
154 154
/// redirect from the old name. A redirect that pointed at the new name
155 155
/// goes, so names never loop.
156 156
pub async fn transfer(pool: &Pool, id: i64, old_owner: &str, name: &str, new_owner: &str) -> sqlx::Result<()> {
157 -
    sqlx::query("UPDATE repositories SET owner = ?, updated_ms = ? WHERE id = ?")
157 +
    sqlx::query("UPDATE repositories SET owner = $1, updated_ms = $2 WHERE id = $3")
158 158
        .bind(new_owner)
159 159
        .bind(now_ms())
160 160
        .bind(id)
161 161
        .execute(pool)
162 162
        .await?;
163 -
    sqlx::query("DELETE FROM repo_redirects WHERE old_owner = ? AND old_name = ?").bind(new_owner).bind(name).execute(pool).await?;
164 -
    sqlx::query("INSERT OR REPLACE INTO repo_redirects (old_owner, old_name, repo_id, moved_ms) VALUES (?, ?, ?, ?)")
163 +
    sqlx::query("DELETE FROM repo_redirects WHERE old_owner = $1 AND old_name = $2").bind(new_owner).bind(name).execute(pool).await?;
164 +
    sqlx::query("INSERT OR REPLACE INTO repo_redirects (old_owner, old_name, repo_id, moved_ms) VALUES ($1, $2, $3, $4)")
165 165
        .bind(old_owner)
166 166
        .bind(name)
167 167
        .bind(id)
…
5 unchanged lines
173 173
174 174
/// Archive (read-only) or bring back a repository.
175 175
pub async fn set_archived(pool: &Pool, id: i64, archived: bool, reason: &str) -> sqlx::Result<()> {
176 -
    sqlx::query("UPDATE repositories SET archived = ?, archived_reason = ?, updated_ms = ? WHERE id = ?")
176 +
    sqlx::query("UPDATE repositories SET archived = $1, archived_reason = $2, updated_ms = $3 WHERE id = $4")
177 177
        .bind(archived)
178 178
        .bind(if archived { reason } else { "" })
179 179
        .bind(now_ms())
…
4 unchanged lines
184 184
}
185 185
186 186
pub async fn delete(pool: &Pool, id: i64) -> sqlx::Result<()> {
187 -
    sqlx::query("DELETE FROM repositories WHERE id = ?").bind(id).execute(pool).await?;
187 +
    sqlx::query("DELETE FROM repositories WHERE id = $1").bind(id).execute(pool).await?;
188 188
    Ok(())
189 189
}
190 190
191 191
pub async fn set_description(pool: &Pool, id: i64, description: &str) -> sqlx::Result<()> {
192 -
    sqlx::query("UPDATE repositories SET description = ? WHERE id = ?").bind(description).bind(id).execute(pool).await?;
192 +
    sqlx::query("UPDATE repositories SET description = $1 WHERE id = $2").bind(description).bind(id).execute(pool).await?;
193 193
    Ok(())
194 194
}
195 195
196 196
pub async fn set_default_bookmark(pool: &Pool, id: i64, bookmark: &str) -> sqlx::Result<()> {
197 -
    sqlx::query("UPDATE repositories SET default_bookmark = ? WHERE id = ?").bind(bookmark).bind(id).execute(pool).await?;
197 +
    sqlx::query("UPDATE repositories SET default_bookmark = $1 WHERE id = $2").bind(bookmark).bind(id).execute(pool).await?;
198 198
    Ok(())
199 199
}
200 200
201 201
pub async fn set_visibility(pool: &Pool, id: i64, visibility: Visibility) -> sqlx::Result<()> {
202 -
    sqlx::query("UPDATE repositories SET visibility = ? WHERE id = ?").bind(visibility.as_str()).bind(id).execute(pool).await?;
202 +
    sqlx::query("UPDATE repositories SET visibility = $1 WHERE id = $2").bind(visibility.as_str()).bind(id).execute(pool).await?;
203 203
    Ok(())
204 204
}
205 205
…
7 unchanged lines
213 213
    let now = now_ms();
214 214
    let mut tx = super::begin_write(pool).await?;
215 215
    for r in results {
216 -
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, account_id, pusher, via) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
216 +
        sqlx::query("INSERT INTO pushes (repo_id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, account_id, pusher, via) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)")
217 217
            .bind(id)
218 218
            .bind(now)
219 219
            .bind(&r.reference)
…
10 unchanged lines
230 230
            .await?;
231 231
    }
232 232
    if results.iter().any(|r| r.ok) {
233 -
        sqlx::query("UPDATE repositories SET updated_ms = ? WHERE id = ?").bind(now).bind(id).execute(&mut *tx).await?;
233 +
        sqlx::query("UPDATE repositories SET updated_ms = $1 WHERE id = $2").bind(now).bind(id).execute(&mut *tx).await?;
234 234
    }
235 235
    tx.commit().await
236 236
}
237 237
238 238
pub async fn pushes(pool: &Pool, id: i64, limit: i64) -> sqlx::Result<Vec<PushRow>> {
239 -
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, pusher FROM pushes WHERE repo_id = ? ORDER BY at_ms DESC, id DESC LIMIT ?")
239 +
    sqlx::query_as("SELECT id, at_ms, reference, ok, reason, old_state, new_state, heads, changes, pusher FROM pushes WHERE repo_id = $1 ORDER BY at_ms DESC, id DESC LIMIT $2")
240 240
        .bind(id)
241 241
        .bind(limit)
242 242
        .fetch_all(pool)
…
20 unchanged lines
263 263
}
264 264
265 265
pub async fn set_topics(pool: &Pool, id: i64, topics: &str) -> sqlx::Result<()> {
266 -
    sqlx::query("UPDATE repositories SET topics = ? WHERE id = ?").bind(topics).bind(id).execute(pool).await?;
266 +
    sqlx::query("UPDATE repositories SET topics = $1 WHERE id = $2").bind(topics).bind(id).execute(pool).await?;
267 267
    Ok(())
268 268
}
269 269
270 270
/// Star or unstar; returns whether it is now starred.
271 271
pub async fn toggle_star(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
272 -
    let gone = sqlx::query("DELETE FROM stars WHERE account_id = ? AND repo_id = ?").bind(account_id).bind(repo_id).execute(pool).await?;
272 +
    let gone = sqlx::query("DELETE FROM stars WHERE account_id = $1 AND repo_id = $2").bind(account_id).bind(repo_id).execute(pool).await?;
273 273
    if gone.rows_affected() > 0 {
274 274
        return Ok(false);
275 275
    }
276 -
    sqlx::query("INSERT INTO stars (account_id, repo_id, at_ms) VALUES (?, ?, ?)")
276 +
    sqlx::query("INSERT INTO stars (account_id, repo_id, at_ms) VALUES ($1, $2, $3)")
277 277
        .bind(account_id)
278 278
        .bind(repo_id)
279 279
        .bind(now_ms())
…
3 unchanged lines
283 283
}
284 284
285 285
pub async fn starred(pool: &Pool, account_id: i64, repo_id: i64) -> sqlx::Result<bool> {
286 -
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE account_id = ? AND repo_id = ?")
286 +
    let n: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE account_id = $1 AND repo_id = $2")
287 287
        .bind(account_id)
288 288
        .bind(repo_id)
289 289
        .fetch_one(pool)
…
3 unchanged lines
293 293
294 294
/// Stars and watchers of one repository.
295 295
pub async fn counts(pool: &Pool, repo_id: i64) -> sqlx::Result<(i64, i64)> {
296 -
    let stars: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE repo_id = ?").bind(repo_id).fetch_one(pool).await?;
296 +
    let stars: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM stars WHERE repo_id = $1").bind(repo_id).fetch_one(pool).await?;
297 297
    let watchers = super::notify::watchers(pool, repo_id).await?.len() as i64;
298 298
    Ok((stars, watchers))
299 299
}
…
6 unchanged lines
306 306
307 307
/// Record that `id` is a fork of `from`.
308 308
pub async fn set_forked_from(pool: &Pool, id: i64, from: i64) -> sqlx::Result<()> {
309 -
    sqlx::query("UPDATE repositories SET forked_from = ? WHERE id = ?").bind(from).bind(id).execute(pool).await?;
309 +
    sqlx::query("UPDATE repositories SET forked_from = $1 WHERE id = $2").bind(from).bind(id).execute(pool).await?;
310 310
    Ok(())
311 311
}
312 312
313 313
/// The forks of `id`, newest activity first.
314 314
pub async fn forks_of(pool: &Pool, id: i64) -> sqlx::Result<Vec<RepoRow>> {
315 -
    sqlx::query_as("SELECT * FROM repositories WHERE forked_from = ? ORDER BY updated_ms DESC").bind(id).fetch_all(pool).await
315 +
    sqlx::query_as("SELECT * FROM repositories WHERE forked_from = $1 ORDER BY updated_ms DESC").bind(id).fetch_all(pool).await
316 316
}
317 317
318 318
/// `on`, `off`, or `` (follow hub.toml's `[flow] repos`).
319 319
pub async fn set_flow(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
320 -
    sqlx::query("UPDATE repositories SET flow = ? WHERE id = ?").bind(setting).bind(id).execute(pool).await?;
320 +
    sqlx::query("UPDATE repositories SET flow = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
321 321
    Ok(())
322 322
}
323 323
324 324
pub async fn set_backup(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
325 -
    sqlx::query("UPDATE repositories SET backup = ? WHERE id = ?").bind(setting).bind(id).execute(pool).await?;
325 +
    sqlx::query("UPDATE repositories SET backup = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
326 326
    Ok(())
327 327
}
328 328
329 329
pub async fn set_diffusion_land(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
330 -
    sqlx::query("UPDATE repositories SET diffusion_land = ? WHERE id = ?").bind(setting).bind(id).execute(pool).await?;
330 +
    sqlx::query("UPDATE repositories SET diffusion_land = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
331 331
    Ok(())
332 332
}
333 333
334 334
pub async fn set_diffusion(pool: &Pool, id: i64, setting: &str) -> sqlx::Result<()> {
335 -
    sqlx::query("UPDATE repositories SET diffusion = ? WHERE id = ?").bind(setting).bind(id).execute(pool).await?;
335 +
    sqlx::query("UPDATE repositories SET diffusion = $1 WHERE id = $2").bind(setting).bind(id).execute(pool).await?;
336 336
    Ok(())
337 337
}
338 338
339 339
pub async fn set_git_mirror(pool: &Pool, id: i64, on: bool) -> sqlx::Result<()> {
340 -
    sqlx::query("UPDATE repositories SET git_mirror = ? WHERE id = ?").bind(on).bind(id).execute(pool).await?;
340 +
    sqlx::query("UPDATE repositories SET git_mirror = $1 WHERE id = $2").bind(on).bind(id).execute(pool).await?;
341 341
    Ok(())
342 342
}
343 343
344 344
pub async fn set_git_mirror_state(pool: &Pool, id: i64, state: &str) -> sqlx::Result<()> {
345 -
    sqlx::query("UPDATE repositories SET git_mirror_state = ? WHERE id = ?").bind(state).bind(id).execute(pool).await?;
345 +
    sqlx::query("UPDATE repositories SET git_mirror_state = $1 WHERE id = $2").bind(state).bind(id).execute(pool).await?;
346 346
    Ok(())
347 347
}
348 348
…
14 unchanged lines
modified crates/hub/src/db/runners.rs +26 −26
…
107 unchanged lines
108 108
}
109 109
110 110
pub async fn by_token(pool: &Pool, token_hash: &str) -> sqlx::Result<Option<Runner>> {
111 -
    sqlx::query_as("SELECT * FROM flow_runners WHERE token_hash = ?").bind(token_hash).fetch_optional(pool).await
111 +
    sqlx::query_as("SELECT * FROM flow_runners WHERE token_hash = $1").bind(token_hash).fetch_optional(pool).await
112 112
}
113 113
114 114
pub async fn get(pool: &Pool, id: i64) -> sqlx::Result<Option<Runner>> {
115 -
    sqlx::query_as("SELECT * FROM flow_runners WHERE id = ?").bind(id).fetch_optional(pool).await
115 +
    sqlx::query_as("SELECT * FROM flow_runners WHERE id = $1").bind(id).fetch_optional(pool).await
116 116
}
117 117
118 118
pub async fn list(pool: &Pool) -> sqlx::Result<Vec<Runner>> {
…
2 unchanged lines
121 121
122 122
/// The row of a runner configured with a hub.toml token, made on first sight.
123 123
pub async fn configured(pool: &Pool, token_hash: &str, name: &str) -> sqlx::Result<Runner> {
124 -
    sqlx::query("INSERT OR IGNORE INTO flow_runners (name, token_hash, source, created_ms) VALUES (?, ?, 'config', ?)")
124 +
    sqlx::query("INSERT OR IGNORE INTO flow_runners (name, token_hash, source, created_ms) VALUES ($1, $2, 'config', $3)")
125 125
        .bind(name)
126 126
        .bind(token_hash)
127 127
        .bind(now_ms())
…
6 unchanged lines
134 134
/// configured runner takes the name it claims with.
135 135
/// The administrator's settings for a runner (#98).
136 136
pub async fn set_limits(pool: &Pool, id: i64, admin_labels: &str, timeout_minutes: Option<i64>) -> sqlx::Result<()> {
137 -
    sqlx::query("UPDATE flow_runners SET admin_labels = ?, timeout_minutes = ? WHERE id = ?")
137 +
    sqlx::query("UPDATE flow_runners SET admin_labels = $1, timeout_minutes = $2 WHERE id = $3")
138 138
        .bind(admin_labels)
139 139
        .bind(timeout_minutes)
140 140
        .bind(id)
…
7 unchanged lines
148 148
    match info {
149 149
        Some(i) => {
150 150
            sqlx::query(
151 -
                "UPDATE flow_runners SET arch = ?, os = ?, version = ?, labels = ?, max_jobs = ?, memory_mib = ?, cpus = ?, last_seen_ms = ? WHERE id = ?",
151 +
                "UPDATE flow_runners SET arch = $1, os = $2, version = $3, labels = $4, max_jobs = $5, memory_mib = $6, cpus = $7, last_seen_ms = $8 WHERE id = $9",
152 152
            )
153 153
            .bind(&i.arch)
154 154
            .bind(&i.os)
…
8 unchanged lines
163 163
            .await?;
164 164
        }
165 165
        None => {
166 -
            sqlx::query("UPDATE flow_runners SET last_seen_ms = ? WHERE id = ?").bind(now).bind(id).execute(pool).await?;
166 +
            sqlx::query("UPDATE flow_runners SET last_seen_ms = $1 WHERE id = $2").bind(now).bind(id).execute(pool).await?;
167 167
        }
168 168
    }
169 169
    if let Some(n) = name {
170 -
        sqlx::query("UPDATE flow_runners SET name = ? WHERE id = ? AND source = 'config'").bind(n).bind(id).execute(pool).await?;
170 +
        sqlx::query("UPDATE flow_runners SET name = $1 WHERE id = $2 AND source = 'config'").bind(n).bind(id).execute(pool).await?;
171 171
    }
172 172
    Ok(())
173 173
}
174 174
175 175
/// Keep what a runner measured about itself at a claim (oxygenflow#40).
176 176
pub async fn set_facts(pool: &Pool, id: i64, f: &flow_core::api::Facts) -> sqlx::Result<()> {
177 -
    sqlx::query("UPDATE flow_runners SET free_disk_mib = ?, load_x100 = ?, speed = ?, speed_class = ?, kernel = ? WHERE id = ?")
177 +
    sqlx::query("UPDATE flow_runners SET free_disk_mib = $1, load_x100 = $2, speed = $3, speed_class = $4, kernel = $5 WHERE id = $6")
178 178
        .bind(f.free_disk_mib.map(|v| v.min(i64::MAX as u64) as i64))
179 179
        .bind(f.load_x100.map(i64::from))
180 180
        .bind(f.speed.map(i64::from))
…
35 unchanged lines
216 216
    profile: &str,
217 217
) -> sqlx::Result<()> {
218 218
    let images = images.map(|i| serde_json::to_string(i).unwrap_or_default()).unwrap_or_default();
219 -
    sqlx::query("UPDATE flow_runners SET tiers = ?, images = ?, fetches_images = ?, profile = ? WHERE id = ?")
219 +
    sqlx::query("UPDATE flow_runners SET tiers = $1, images = $2, fetches_images = $3, profile = $4 WHERE id = $5")
220 220
        .bind(tiers.join(","))
221 221
        .bind(images)
222 222
        .bind(fetches_images)
…
20 unchanged lines
243 243
}
244 244
245 245
pub async fn set_state(pool: &Pool, id: i64, state: &str) -> sqlx::Result<()> {
246 -
    sqlx::query("UPDATE flow_runners SET state = ?, state_ms = ? WHERE id = ?")
246 +
    sqlx::query("UPDATE flow_runners SET state = $1, state_ms = $2 WHERE id = $3")
247 247
        .bind(state)
248 248
        .bind(super::now_ms())
249 249
        .bind(id)
…
10 unchanged lines
260 260
    }
261 261
    let last: Vec<(String, String)> = sqlx::query_as(
262 262
        "SELECT j.status, j.error FROM flow_jobs j JOIN flow_runners r ON r.id = j.runner_id
263 -
         WHERE j.runner_id = ? AND j.finished_ms IS NOT NULL AND j.finished_ms >= r.state_ms
264 -
         ORDER BY j.finished_ms DESC, j.id DESC LIMIT ?",
263 +
         WHERE j.runner_id = $1 AND j.finished_ms IS NOT NULL AND j.finished_ms >= r.state_ms
264 +
         ORDER BY j.finished_ms DESC, j.id DESC LIMIT $2",
265 265
    )
266 266
    .bind(id)
267 267
    .bind(n as i64)
…
4 unchanged lines
272 272
273 273
/// Forget a runner: its credential stops working. Its jobs keep its name.
274 274
pub async fn remove(pool: &Pool, id: i64) -> sqlx::Result<()> {
275 -
    sqlx::query("DELETE FROM flow_runners WHERE id = ?").bind(id).execute(pool).await?;
275 +
    sqlx::query("DELETE FROM flow_runners WHERE id = $1").bind(id).execute(pool).await?;
276 276
    Ok(())
277 277
}
278 278
279 279
/// The key a runner signs receipts with: the one bound, or `key` (named
280 280
/// `name`) when none is yet.
281 281
pub async fn bind_key(pool: &Pool, id: i64, key: &str, name: &str) -> sqlx::Result<String> {
282 -
    sqlx::query("UPDATE flow_runners SET key = ?, key_name = ? WHERE id = ? AND key = ''")
282 +
    sqlx::query("UPDATE flow_runners SET key = $1, key_name = $2 WHERE id = $3 AND key = ''")
283 283
        .bind(key)
284 284
        .bind(name)
285 285
        .bind(id)
286 286
        .execute(pool)
287 287
        .await?;
288 -
    sqlx::query_scalar("SELECT key FROM flow_runners WHERE id = ?").bind(id).fetch_one(pool).await
288 +
    sqlx::query_scalar("SELECT key FROM flow_runners WHERE id = $1").bind(id).fetch_one(pool).await
289 289
}
290 290
291 291
/// Keys that runners of this hub sign with.
…
5 unchanged lines
297 297
pub async fn add_registration(pool: &Pool, token_hash: &str, by: &str, scope: &Scope) -> sqlx::Result<()> {
298 298
    let now = now_ms();
299 299
    sqlx::query(
300 -
        "INSERT INTO flow_runner_registrations (token_hash, created_by, created_ms, expires_ms, scope_owner, scope_repo_id) VALUES (?, ?, ?, ?, ?, ?)",
300 +
        "INSERT INTO flow_runner_registrations (token_hash, created_by, created_ms, expires_ms, scope_owner, scope_repo_id) VALUES ($1, $2, $3, $4, $5, $6)",
301 301
    )
302 302
    .bind(token_hash)
303 303
    .bind(by)
…
11 unchanged lines
315 315
pub async fn serving(pool: &Pool, owner: &str, repo_id: i64) -> sqlx::Result<Vec<Runner>> {
316 316
    sqlx::query_as(
317 317
        "SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) \
318 -
         OR (scope_owner = ? AND scope_repo_id IS NULL) OR scope_repo_id = ? ORDER BY name",
318 +
         OR (scope_owner = $1 AND scope_repo_id IS NULL) OR scope_repo_id = $2 ORDER BY name",
319 319
    )
320 320
    .bind(owner)
321 321
    .bind(repo_id)
…
4 unchanged lines
326 326
/// The runners that serve an owner: the site's, and those scoped to the
327 327
/// owner or one of its repositories.
328 328
pub async fn serving_owner(pool: &Pool, owner: &str) -> sqlx::Result<Vec<Runner>> {
329 -
    sqlx::query_as("SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) OR scope_owner = ? ORDER BY name")
329 +
    sqlx::query_as("SELECT * FROM flow_runners WHERE (scope_owner = '' AND scope_repo_id IS NULL) OR scope_owner = $1 ORDER BY name")
330 330
        .bind(owner)
331 331
        .fetch_all(pool)
332 332
        .await
…
1 unchanged lines
334 334
335 335
/// Change the repositories a runner serves.
336 336
pub async fn set_scope(pool: &Pool, id: i64, scope: &Scope) -> sqlx::Result<()> {
337 -
    sqlx::query("UPDATE flow_runners SET scope_owner = ?, scope_repo_id = ? WHERE id = ?")
337 +
    sqlx::query("UPDATE flow_runners SET scope_owner = $1, scope_repo_id = $2 WHERE id = $3")
338 338
        .bind(&scope.owner)
339 339
        .bind(scope.repo_id)
340 340
        .bind(id)
…
4 unchanged lines
345 345
346 346
/// Registration tokens not used yet and not expired: (created by, created, expires).
347 347
pub async fn pending_registrations(pool: &Pool) -> sqlx::Result<Vec<(String, i64, i64)>> {
348 -
    sqlx::query_as("SELECT created_by, created_ms, expires_ms FROM flow_runner_registrations WHERE used_ms IS NULL AND expires_ms > ? ORDER BY created_ms DESC")
348 +
    sqlx::query_as("SELECT created_by, created_ms, expires_ms FROM flow_runner_registrations WHERE used_ms IS NULL AND expires_ms > $1 ORDER BY created_ms DESC")
349 349
        .bind(now_ms())
350 350
        .fetch_all(pool)
351 351
        .await
…
14 unchanged lines
366 366
    let mut tx = super::begin_write(pool).await?;
367 367
    let now = now_ms();
368 368
    let found: Option<(String, String, Option<i64>)> = sqlx::query_as(
369 -
        "SELECT created_by, scope_owner, scope_repo_id FROM flow_runner_registrations WHERE token_hash = ? AND used_ms IS NULL AND expires_ms > ?",
369 +
        "SELECT created_by, scope_owner, scope_repo_id FROM flow_runner_registrations WHERE token_hash = $1 AND used_ms IS NULL AND expires_ms > $2",
370 370
    )
371 371
    .bind(registration)
372 372
    .bind(now)
…
1 unchanged lines
374 374
    .await?;
375 375
    let Some((who, scope_owner, scope_repo_id)) = found else { return Ok(None) };
376 376
    let id: i64 = sqlx::query_scalar(
377 -
        "INSERT INTO flow_runners (name, token_hash, source, key, key_name, created_ms, created_by, scope_owner, scope_repo_id) VALUES (?, ?, 'registered', ?, ?, ?, ?, ?, ?) RETURNING id",
377 +
        "INSERT INTO flow_runners (name, token_hash, source, key, key_name, created_ms, created_by, scope_owner, scope_repo_id) VALUES ($1, $2, 'registered', $3, $4, $5, $6, $7, $8) RETURNING id",
378 378
    )
379 379
    .bind(name)
380 380
    .bind(token_hash)
…
5 unchanged lines
386 386
    .bind(scope_repo_id)
387 387
    .fetch_one(&mut *tx)
388 388
    .await?;
389 -
    sqlx::query("UPDATE flow_runner_registrations SET used_ms = ?, runner_id = ? WHERE token_hash = ?")
389 +
    sqlx::query("UPDATE flow_runner_registrations SET used_ms = $1, runner_id = $2 WHERE token_hash = $3")
390 390
        .bind(now)
391 391
        .bind(id)
392 392
        .bind(registration)
…
22 unchanged lines
415 415
        "SELECT p.owner AS owner, p.name AS repo, r.number AS run_number, j.name AS job, j.status AS status, j.started_ms AS started_ms, \
416 416
         j.finished_ms AS finished_ms, j.receipt_attest AS receipt_attest \
417 417
         FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id \
418 -
         WHERE j.runner_id = ? AND (? IS NULL OR j.started_ms < ?) ORDER BY j.started_ms DESC LIMIT ?",
418 +
         WHERE j.runner_id = $1 AND ($2 IS NULL OR j.started_ms < $3) ORDER BY j.started_ms DESC LIMIT $4",
419 419
    )
420 420
    .bind(id)
421 421
    .bind(before)
…
7 unchanged lines
429 429
pub async fn totals(pool: &Pool, id: i64) -> sqlx::Result<(i64, i64, i64)> {
430 430
    sqlx::query_as(
431 431
        "SELECT COUNT(*), COALESCE(SUM(status = 'passed'), 0), COALESCE(SUM(finished_ms - started_ms), 0) \
432 -
         FROM flow_jobs WHERE runner_id = ? AND finished_ms IS NOT NULL",
432 +
         FROM flow_jobs WHERE runner_id = $1 AND finished_ms IS NOT NULL",
433 433
    )
434 434
    .bind(id)
435 435
    .fetch_one(pool)
…
8 unchanged lines
444 444
}
445 445
446 446
pub async fn set_job_runner(pool: &Pool, job_id: i64, runner_id: i64) -> sqlx::Result<()> {
447 -
    sqlx::query("UPDATE flow_jobs SET runner_id = ? WHERE id = ?").bind(runner_id).bind(job_id).execute(pool).await?;
447 +
    sqlx::query("UPDATE flow_jobs SET runner_id = $1 WHERE id = $2").bind(runner_id).bind(job_id).execute(pool).await?;
448 448
    Ok(())
449 449
}
modified crates/hub/src/db/sessions.rs +9 −9
…
31 unchanged lines
32 32
33 33
pub async fn create(pool: &Pool, account_id: i64, hash: &str, salt: &str) -> sqlx::Result<i64> {
34 34
    let now = now_ms();
35 -
    let r = sqlx::query("INSERT INTO sessions (account_id, hash, salt, created_ms, seen_ms) VALUES (?, ?, ?, ?, ?)")
35 +
    let r = sqlx::query("INSERT INTO sessions (account_id, hash, salt, created_ms, seen_ms) VALUES ($1, $2, $3, $4, $5)")
36 36
        .bind(account_id)
37 37
        .bind(hash)
38 38
        .bind(salt)
…
5 unchanged lines
44 44
}
45 45
46 46
pub async fn by_hash(pool: &Pool, hash: &str) -> sqlx::Result<Option<Session>> {
47 -
    sqlx::query_as(&format!("SELECT {COLUMNS} FROM sessions WHERE hash = ?")).bind(hash).fetch_optional(pool).await
47 +
    sqlx::query_as(&format!("SELECT {COLUMNS} FROM sessions WHERE hash = $1")).bind(hash).fetch_optional(pool).await
48 48
}
49 49
50 50
/// Note a use: the time (at most once a minute) and, the first time, the
51 51
/// address and browser it came from.
52 52
pub async fn touch(pool: &Pool, s: &Session, address: &str, agent: &str, now: i64) -> sqlx::Result<()> {
53 53
    if s.agent.is_empty() && s.address.is_empty() && (!agent.is_empty() || !address.is_empty()) {
54 -
        sqlx::query("UPDATE sessions SET seen_ms = ?, address = ?, agent = ? WHERE id = ?")
54 +
        sqlx::query("UPDATE sessions SET seen_ms = $1, address = $2, agent = $3 WHERE id = $4")
55 55
            .bind(now)
56 56
            .bind(address)
57 57
            .bind(agent)
…
1 unchanged lines
59 59
            .execute(pool)
60 60
            .await?;
61 61
    } else if now - s.seen_ms >= TOUCH_EVERY_MS {
62 -
        sqlx::query("UPDATE sessions SET seen_ms = ? WHERE id = ?").bind(now).bind(s.id).execute(pool).await?;
62 +
        sqlx::query("UPDATE sessions SET seen_ms = $1 WHERE id = $2").bind(now).bind(s.id).execute(pool).await?;
63 63
    }
64 64
    Ok(())
65 65
}
…
1 unchanged lines
67 67
/// An account's live sessions, most recently used first.
68 68
pub async fn of_account(pool: &Pool, account_id: i64, salt: &str) -> sqlx::Result<Vec<Session>> {
69 69
    let now = now_ms();
70 -
    let all: Vec<Session> = sqlx::query_as(&format!("SELECT {COLUMNS} FROM sessions WHERE account_id = ? ORDER BY seen_ms DESC"))
70 +
    let all: Vec<Session> = sqlx::query_as(&format!("SELECT {COLUMNS} FROM sessions WHERE account_id = $1 ORDER BY seen_ms DESC"))
71 71
        .bind(account_id)
72 72
        .fetch_all(pool)
73 73
        .await?;
…
2 unchanged lines
76 76
77 77
/// End one session of an account's; whether there was one.
78 78
pub async fn end(pool: &Pool, account_id: i64, id: i64) -> sqlx::Result<bool> {
79 -
    let r = sqlx::query("DELETE FROM sessions WHERE id = ? AND account_id = ?").bind(id).bind(account_id).execute(pool).await?;
79 +
    let r = sqlx::query("DELETE FROM sessions WHERE id = $1 AND account_id = $2").bind(id).bind(account_id).execute(pool).await?;
80 80
    Ok(r.rows_affected() > 0)
81 81
}
82 82
83 83
pub async fn end_hash(pool: &Pool, hash: &str) -> sqlx::Result<()> {
84 -
    sqlx::query("DELETE FROM sessions WHERE hash = ?").bind(hash).execute(pool).await?;
84 +
    sqlx::query("DELETE FROM sessions WHERE hash = $1").bind(hash).execute(pool).await?;
85 85
    Ok(())
86 86
}
87 87
88 88
/// Every session of an account's but `keep`.
89 89
pub async fn end_others(pool: &Pool, account_id: i64, keep: i64) -> sqlx::Result<()> {
90 -
    sqlx::query("DELETE FROM sessions WHERE account_id = ? AND id != ?").bind(account_id).bind(keep).execute(pool).await?;
90 +
    sqlx::query("DELETE FROM sessions WHERE account_id = $1 AND id != $2").bind(account_id).bind(keep).execute(pool).await?;
91 91
    Ok(())
92 92
}
93 93
…
1 unchanged lines
95 95
pub async fn sweep(pool: &Pool) -> sqlx::Result<u64> {
96 96
    let now = now_ms();
97 97
    let r = sqlx::query(
98 -
        "DELETE FROM sessions WHERE seen_ms < ? OR created_ms < ? \
98 +
        "DELETE FROM sessions WHERE seen_ms < $1 OR created_ms < $2 \
99 99
         OR salt != (SELECT session_salt FROM accounts WHERE accounts.id = sessions.account_id)",
100 100
    )
101 101
    .bind(now - IDLE_MS)
…
23 unchanged lines
modified crates/hub/src/diffusion.rs +32 −30
…
22 unchanged lines
23 23
pub async fn target(pool: &Pool, row: &RepoRow) -> String {
24 24
    let since = db::now_ms() - LEARN_DAYS * 24 * 3600 * 1000;
25 25
    let learned: Option<String> = sqlx::query_scalar(
26 -
        "SELECT target FROM proposals WHERE repo_id = ? AND state = 'landed' AND landed_ms >= ?
26 +
        "SELECT target FROM proposals WHERE repo_id = $1 AND state = 'landed' AND landed_ms >= $2
27 27
         GROUP BY target ORDER BY COUNT(*) DESC, MAX(landed_ms) DESC LIMIT 1",
28 28
    )
29 29
    .bind(row.id)
…
94 unchanged lines
124 124
        return Err("begin: forced for a test".into());
125 125
    }
126 126
    let mut tx = app.db.begin().await.map_err(|e| format!("begin: {e}"))?;
127 -
    sqlx::query("DELETE FROM diffusion_pins WHERE consumer_id = ? AND bookmark = ?")
127 +
    sqlx::query("DELETE FROM diffusion_pins WHERE consumer_id = $1 AND bookmark = $2")
128 128
        .bind(row.id)
129 129
        .bind(bookmark)
130 130
        .execute(&mut *tx)
…
2 unchanged lines
133 133
    for ((path, id, pinned), upstream_row) in pins.iter().zip(&upstreams) {
134 134
        let heads: Vec<String> = pinned.iter().map(|h| h.to_hex()).collect();
135 135
        sqlx::query(
136 -
            "INSERT INTO diffusion_pins (consumer_id, bookmark, path, upstream, heads, updated_ms, upstream_repo_id) VALUES (?, ?, ?, ?, ?, ?, ?)",
136 +
            "INSERT INTO diffusion_pins (consumer_id, bookmark, path, upstream, heads, updated_ms, upstream_repo_id) VALUES ($1, $2, $3, $4, $5, $6, $7)",
137 137
        )
138 138
        .bind(row.id)
139 139
        .bind(bookmark)
…
6 unchanged lines
146 146
        .await
147 147
        .map_err(|e| format!("insert {path}: {e}"))?;
148 148
    }
149 -
    sqlx::query("INSERT OR REPLACE INTO diffusion_indexed (consumer_id, bookmark, heads, updated_ms) VALUES (?, ?, ?, ?)")
149 +
    sqlx::query("INSERT OR REPLACE INTO diffusion_indexed (consumer_id, bookmark, heads, updated_ms) VALUES ($1, $2, $3, $4)")
150 150
        .bind(row.id)
151 151
        .bind(bookmark)
152 152
        .bind(heads_key(heads))
…
94 unchanged lines
247 247
        for bookmark in followed_bookmarks(&app.db, row).await {
248 248
            let b = bookmark.clone();
249 249
            let Ok(heads) = app.repos.read(&name, move |r, _| heads_of(r, &b)).await else { continue };
250 -
            let indexed: Option<String> = sqlx::query_scalar("SELECT heads FROM diffusion_indexed WHERE consumer_id = ? AND bookmark = ?")
251 -
                .bind(row.id)
252 -
                .bind(&bookmark)
253 -
                .fetch_optional(&app.db)
254 -
                .await
255 -
                .ok()
256 -
                .flatten();
250 +
            let indexed: Option<String> =
251 +
                sqlx::query_scalar("SELECT heads FROM diffusion_indexed WHERE consumer_id = $1 AND bookmark = $2")
252 +
                    .bind(row.id)
253 +
                    .bind(&bookmark)
254 +
                    .fetch_optional(&app.db)
255 +
                    .await
256 +
                    .ok()
257 +
                    .flatten();
257 258
            if indexed.as_deref() != Some(heads_key(&heads).as_str()) {
258 259
                tracing::info!("diffusion: {}/{} {bookmark}: the index was not at the bookmark; re-indexing", row.owner, row.name);
259 260
                record(app, row, &bookmark, &heads).await;
…
213 unchanged lines
473 474
    let Some(name) = RepoName::new(&row.owner, &row.name) else { return Ok(Vec::new()) };
474 475
    let id = app.repos.read(&name, |r, _| Ok(r.id()?)).await?;
475 476
    let found: Vec<(i64, String, String, String)> = sqlx::query_as(
476 -
        "SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = ? ORDER BY consumer_id, bookmark, path",
477 +
        "SELECT consumer_id, bookmark, path, heads FROM diffusion_pins WHERE upstream = $1 ORDER BY consumer_id, bookmark, path",
477 478
    )
478 479
    .bind(hex::encode(id))
479 480
    .fetch_all(&app.db)
…
23 unchanged lines
503 504
        let bump = mine.iter().find(|p| p.state == "open" || p.state == "draft").map(|p| p.number);
504 505
        let landed = mine.iter().filter(|p| p.state == "landed").max_by_key(|p| p.landed_ms).map(|p| (p.number, p.landed_ms));
505 506
        let run: Option<(i64, String)> =
506 -
            sqlx::query_as("SELECT number, status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1")
507 +
            sqlx::query_as("SELECT number, status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1")
507 508
                .bind(c.id)
508 509
                .bind(&bump_bm)
509 510
                .fetch_optional(&app.db)
…
33 unchanged lines
543 544
        })
544 545
        .await?;
545 546
    let up_run: Option<String> =
546 -
        sqlx::query_scalar("SELECT status FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1")
547 +
        sqlx::query_scalar("SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1")
547 548
            .bind(row.id)
548 549
            .bind(&row.default_bookmark)
549 550
            .fetch_optional(&app.db)
…
15 unchanged lines
565 566
/// this hub it pins, with its status there.
566 567
pub async fn follows(app: &Shared, consumer: &RepoRow, account: Option<&Account>) -> Vec<(RepoRow, Consumer)> {
567 568
    let ups: Vec<i64> =
568 -
        sqlx::query_scalar("SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE consumer_id = ? AND upstream_repo_id IS NOT NULL")
569 +
        sqlx::query_scalar("SELECT DISTINCT upstream_repo_id FROM diffusion_pins WHERE consumer_id = $1 AND upstream_repo_id IS NOT NULL")
569 570
            .bind(consumer.id)
570 571
            .fetch_all(&app.db)
571 572
            .await
…
471 unchanged lines
1043 1044
async fn propagate_to(app: &Shared, upstream: &RepoRow, heads: Vec<ChangeHash>, green: bool, only: Option<i64>) {
1044 1045
    let Some(uname) = RepoName::new(&upstream.owner, &upstream.name) else { return };
1045 1046
    let Ok(id) = app.repos.read(&uname, |r, _| Ok(r.id()?)).await else { return };
1046 -
    let rows: Vec<(i64, String, String)> = sqlx::query_as("SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = ?")
1047 +
    let rows: Vec<(i64, String, String)> = sqlx::query_as("SELECT consumer_id, bookmark, path FROM diffusion_pins WHERE upstream = $1")
1047 1048
        .bind(hex::encode(id))
1048 1049
        .fetch_all(&app.db)
1049 1050
        .await
…
101 unchanged lines
1151 1152
        return Err(Held::Manual);
1152 1153
    }
1153 1154
    let upstream_id: Option<i64> = sqlx::query_scalar(
1154 -
        "SELECT upstream_repo_id FROM diffusion_pins WHERE consumer_id = ? AND path = ? AND upstream_repo_id IS NOT NULL LIMIT 1",
1155 +
        "SELECT upstream_repo_id FROM diffusion_pins WHERE consumer_id = $1 AND path = $2 AND upstream_repo_id IS NOT NULL LIMIT 1",
1155 1156
    )
1156 1157
    .bind(consumer.id)
1157 1158
    .bind(path)
…
40 unchanged lines
1198 1199
        a == b
1199 1200
    };
1200 1201
    let upstream_runs: Vec<String> = sqlx::query_scalar(
1201 -
        "SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 100",
1202 +
        "SELECT heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND status = 'passed' ORDER BY id DESC LIMIT 100",
1202 1203
    )
1203 1204
    .bind(upstream.id)
1204 1205
    .bind(&upstream.default_bookmark)
…
4 unchanged lines
1209 1210
        return Err(Held::UpstreamUnchecked);
1210 1211
    }
1211 1212
    let run: Option<(String, String)> =
1212 -
        sqlx::query_as("SELECT status, heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? ORDER BY id DESC LIMIT 1")
1213 +
        sqlx::query_as("SELECT status, heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 ORDER BY id DESC LIMIT 1")
1213 1214
            .bind(consumer.id)
1214 1215
            .bind(&bm)
1215 1216
            .fetch_optional(&app.db)
…
39 unchanged lines
1255 1256
1256 1257
/// Try every open bump of `consumer` (its setting turned to `auto`).
1257 1258
pub async fn land_ready(app: &Shared, consumer: &RepoRow) {
1258 -
    let paths: Vec<String> = sqlx::query_scalar("SELECT DISTINCT path FROM diffusion_pins WHERE consumer_id = ?")
1259 +
    let paths: Vec<String> = sqlx::query_scalar("SELECT DISTINCT path FROM diffusion_pins WHERE consumer_id = $1")
1259 1260
        .bind(consumer.id)
1260 1261
        .fetch_all(&app.db)
1261 1262
        .await
…
10 unchanged lines
1272 1273
    if !crate::flow::enabled(app, upstream) {
1273 1274
        return true;
1274 1275
    }
1275 -
    let runs: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND bookmark = ?")
1276 +
    let runs: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM flow_runs WHERE repo_id = $1 AND bookmark = $2")
1276 1277
        .bind(upstream.id)
1277 1278
        .bind(&upstream.default_bookmark)
1278 1279
        .fetch_one(&app.db)
…
15 unchanged lines
1294 1295
1295 1296
/// The newest state of `upstream`'s default bookmark whose run passed.
1296 1297
async fn last_green(app: &Shared, upstream: &RepoRow) -> Option<Vec<ChangeHash>> {
1297 -
    let heads: String =
1298 -
        sqlx::query_scalar("SELECT heads FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND status = 'passed' ORDER BY id DESC LIMIT 1")
1299 -
            .bind(upstream.id)
1300 -
            .bind(&upstream.default_bookmark)
1301 -
            .fetch_optional(&app.db)
1302 -
            .await
1303 -
            .ok()
1304 -
            .flatten()?;
1298 +
    let heads: String = sqlx::query_scalar(
1299 +
        "SELECT heads FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND status = 'passed' ORDER BY id DESC LIMIT 1",
1300 +
    )
1301 +
    .bind(upstream.id)
1302 +
    .bind(&upstream.default_bookmark)
1303 +
    .fetch_optional(&app.db)
1304 +
    .await
1305 +
    .ok()
1306 +
    .flatten()?;
1305 1307
    let h: Vec<ChangeHash> = heads.split('+').filter_map(ChangeHash::from_hex).collect();
1306 1308
    (!h.is_empty()).then_some(h)
1307 1309
}
…
2 unchanged lines
1310 1312
/// it turns Diffusion on, it is bumped to each upstream's newest state
1311 1313
/// that passed (or simply its newest, when it follows every move).
1312 1314
pub async fn catch_up(app: &Shared, consumer: &RepoRow) {
1313 -
    let upstreams: Vec<String> = sqlx::query_scalar("SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = ?")
1315 +
    let upstreams: Vec<String> = sqlx::query_scalar("SELECT DISTINCT upstream FROM diffusion_pins WHERE consumer_id = $1")
1314 1316
        .bind(consumer.id)
1315 1317
        .fetch_all(&app.db)
1316 1318
        .await
…
217 unchanged lines
modified crates/hub/src/flow.rs +4 −4
…
1452 unchanged lines
1453 1453
                // pin), and no other (#150).
1454 1454
                let diffused = run.actor == crate::diffusion::ACCOUNT
1455 1455
                    && run.bookmark.starts_with("diffusion/")
1456 -
                    && sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = ? AND upstream_repo_id = ?")
1456 +
                    && sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM diffusion_pins WHERE consumer_id = $1 AND upstream_repo_id = $2")
1457 1457
                        .bind(run.repo_id)
1458 1458
                        .bind(other.id)
1459 1459
                        .fetch_one(&app.db)
…
523 unchanged lines
1983 1983
        // A pass after a failure on the same bookmark: fixed (#183).
1984 1984
        if done.status == "passed" {
1985 1985
            let before: Option<String> = sqlx::query_scalar(
1986 -
                "SELECT status FROM flow_runs WHERE repo_id = ? AND bookmark = ? AND id < ? AND status IN ('passed', 'failed')
1986 +
                "SELECT status FROM flow_runs WHERE repo_id = $1 AND bookmark = $2 AND id < $3 AND status IN ('passed', 'failed')
1987 1987
                 ORDER BY id DESC LIMIT 1",
1988 1988
            )
1989 1989
            .bind(done.repo_id)
…
3 unchanged lines
1993 1993
            .await?;
1994 1994
            // Or this very run failed and a job of it was run again.
1995 1995
            let url = format!("/{}/{}/flow/{}", row.owner, row.name, done.number);
1996 -
            let failed_here: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE kind = 'flow.failed' AND url = ?")
1996 +
            let failed_here: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM notifications WHERE kind = 'flow.failed' AND url = $1")
1997 1997
                .bind(&url)
1998 1998
                .fetch_one(&app.db)
1999 1999
                .await?;
…
31 unchanged lines
2031 2031
}
2032 2032
2033 2033
async fn runner_of(app: &Shared, job_id: i64) -> Option<i64> {
2034 -
    sqlx::query_scalar("SELECT runner_id FROM flow_jobs WHERE id = ?").bind(job_id).fetch_optional(&app.db).await.ok().flatten().flatten()
2034 +
    sqlx::query_scalar("SELECT runner_id FROM flow_jobs WHERE id = $1").bind(job_id).fetch_optional(&app.db).await.ok().flatten().flatten()
2035 2035
}
2036 2036
2037 2037
/// A failed run (oxygenflow#30): its pusher and the repository's watchers
…
451 unchanged lines
modified crates/hub/src/images.rs +3 −3
…
165 unchanged lines
166 166
/// Record an image this hub now keeps.
167 167
pub async fn record(pool: &Pool, m: &Manifest, uploader: &str) -> sqlx::Result<()> {
168 168
    sqlx::query(
169 -
        "INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES (?, ?, ?, ?, ?, ?) \
169 +
        "INSERT INTO jet_images (hash, size, arch, chunks, uploader, created_ms) VALUES ($1, $2, $3, $4, $5, $6) \
170 170
         ON CONFLICT (hash) DO UPDATE SET arch = excluded.arch WHERE excluded.arch != ''",
171 171
    )
172 172
    .bind(&m.hash)
…
29 unchanged lines
202 202
        .ok()
203 203
        .flatten();
204 204
        if let Some(a) = arch {
205 -
            sqlx::query("UPDATE jet_images SET arch = ? WHERE hash = ?").bind(a).bind(&k.hash).execute(&app.db).await?;
205 +
            sqlx::query("UPDATE jet_images SET arch = $1 WHERE hash = $2").bind(a).bind(&k.hash).execute(&app.db).await?;
206 206
            crate::db::flow::record_image_archs(&app.db, &[(k.hash.clone(), a.to_string())].into_iter().collect()).await?;
207 207
            read += 1;
208 208
        }
…
39 unchanged lines
248 248
        "SELECT lower(json_extract(j.sandbox, '$.rootfs')) AS image, p.owner, p.name, COUNT(*) AS jobs,
249 249
                MAX(COALESCE(j.started_ms, r.created_ms)) AS last_ms
250 250
         FROM flow_jobs j JOIN flow_runs r ON r.id = j.run_id JOIN repositories p ON p.id = r.repo_id
251 -
         WHERE r.created_ms >= ? AND COALESCE(json_extract(j.sandbox, '$.rootfs'), '') != ''
251 +
         WHERE r.created_ms >= $1 AND COALESCE(json_extract(j.sandbox, '$.rootfs'), '') != ''
252 252
         GROUP BY image, p.id ORDER BY last_ms DESC",
253 253
    )
254 254
    .bind(since_ms)
…
8 unchanged lines
modified crates/hub/src/junit.rs +22 −20
…
275 unchanged lines
276 276
pub async fn keep(app: &Shared, o: &Origin<'_>, cases: &[Case]) -> sqlx::Result<i64> {
277 277
    let count = |s: &str| cases.iter().filter(|c| c.status == s).count() as i64;
278 278
    let mut tx = crate::db::begin_write(&app.db).await?;
279 -
    sqlx::query("DELETE FROM test_reports WHERE job_id = ?").bind(o.job_id).execute(&mut *tx).await?;
279 +
    sqlx::query("DELETE FROM test_reports WHERE job_id = $1").bind(o.job_id).execute(&mut *tx).await?;
280 280
    let id: i64 = sqlx::query_scalar(
281 281
        "INSERT INTO test_reports (repo_id, run_id, job_id, job, run_number, bookmark, state, blob, \
282 282
         tests, passed, failed, errors, skipped, time_ms, created_ms) \
283 -
         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
283 +
         VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) RETURNING id",
284 284
    )
285 285
    .bind(o.repo_id)
286 286
    .bind(o.run_id)
…
15 unchanged lines
302 302
    for (i, c) in cases.iter().enumerate() {
303 303
        sqlx::query(
304 304
            "INSERT INTO test_cases (report_id, idx, suite, classname, name, status, time_ms, message, output) \
305 -
             VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
305 +
             VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)",
306 306
        )
307 307
        .bind(id)
308 308
        .bind(i as i64)
…
14 unchanged lines
323 323
324 324
/// A run's reports, one per job that kept one.
325 325
pub async fn for_run(pool: &Pool, run_id: i64) -> sqlx::Result<Vec<Report>> {
326 -
    sqlx::query_as("SELECT * FROM test_reports WHERE run_id = ? ORDER BY job").bind(run_id).fetch_all(pool).await
326 +
    sqlx::query_as("SELECT * FROM test_reports WHERE run_id = $1 ORDER BY job").bind(run_id).fetch_all(pool).await
327 327
}
328 328
329 329
/// A report's cases in report order, all or only those with a status
…
2 unchanged lines
332 332
    let filter = if statuses.is_empty() {
333 333
        String::new()
334 334
    } else {
335 -
        format!(" AND status IN ({})", statuses.iter().map(|_| "?").collect::<Vec<_>>().join(", "))
335 +
        // $1 is the report; the statuses follow, then the limit.
336 +
        format!(" AND status IN ({})", (2..2 + statuses.len()).map(|i| format!("${i}")).collect::<Vec<_>>().join(", "))
336 337
    };
338 +
    let limit_at = 2 + statuses.len();
337 339
    let sql = format!(
338 -
        "SELECT suite, classname, name, status, time_ms, message, output FROM test_cases WHERE report_id = ?{filter} ORDER BY idx LIMIT ?"
340 +
        "SELECT suite, classname, name, status, time_ms, message, output FROM test_cases WHERE report_id = $1{filter} ORDER BY idx LIMIT ${limit_at}"
339 341
    );
340 342
    let mut q = sqlx::query_as(&sql).bind(report_id);
341 343
    for s in statuses {
…
6 unchanged lines
348 350
pub async fn failures(pool: &Pool, report_id: i64, limit: i64) -> sqlx::Result<Vec<Case>> {
349 351
    sqlx::query_as(
350 352
        "SELECT suite, classname, name, status, time_ms, message, output FROM test_cases \
351 -
         WHERE report_id = ? AND status IN ('failed', 'error') ORDER BY idx LIMIT ?",
353 +
         WHERE report_id = $1 AND status IN ('failed', 'error') ORDER BY idx LIMIT $2",
352 354
    )
353 355
    .bind(report_id)
354 356
    .bind(limit)
…
27 unchanged lines
382 384
}
383 385
384 386
pub async fn report(pool: &Pool, repo_id: i64, id: i64) -> sqlx::Result<Option<Report>> {
385 -
    sqlx::query_as("SELECT * FROM test_reports WHERE id = ? AND repo_id = ?").bind(id).bind(repo_id).fetch_optional(pool).await
387 +
    sqlx::query_as("SELECT * FROM test_reports WHERE id = $1 AND repo_id = $2").bind(id).bind(repo_id).fetch_optional(pool).await
386 388
}
387 389
388 390
/// The newest reports on a bookmark, newest first.
389 391
pub async fn on_bookmark(pool: &Pool, repo_id: i64, bookmark: &str, limit: i64) -> sqlx::Result<Vec<Report>> {
390 -
    sqlx::query_as("SELECT * FROM test_reports WHERE repo_id = ? AND bookmark = ? ORDER BY created_ms DESC, id DESC LIMIT ?")
392 +
    sqlx::query_as("SELECT * FROM test_reports WHERE repo_id = $1 AND bookmark = $2 ORDER BY created_ms DESC, id DESC LIMIT $3")
391 393
        .bind(repo_id)
392 394
        .bind(bookmark)
393 395
        .bind(limit)
…
4 unchanged lines
398 400
/// Bookmarks with reports, the most recently reported first.
399 401
pub async fn bookmarks(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
400 402
    let rows: Vec<(String,)> =
401 -
        sqlx::query_as("SELECT bookmark FROM test_reports WHERE repo_id = ? GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
403 +
        sqlx::query_as("SELECT bookmark FROM test_reports WHERE repo_id = $1 GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
402 404
            .bind(repo_id)
403 405
            .fetch_all(pool)
404 406
            .await?;
…
1 unchanged lines
406 408
}
407 409
408 410
pub async fn first_ms(pool: &Pool, repo_id: i64) -> sqlx::Result<Option<i64>> {
409 -
    sqlx::query_scalar("SELECT MIN(created_ms) FROM test_reports WHERE repo_id = ?").bind(repo_id).fetch_one(pool).await
411 +
    sqlx::query_scalar("SELECT MIN(created_ms) FROM test_reports WHERE repo_id = $1").bind(repo_id).fetch_one(pool).await
410 412
}
411 413
412 414
/// A report's slowest cases.
413 415
pub async fn slowest(pool: &Pool, report_id: i64, limit: i64) -> sqlx::Result<Vec<Case>> {
414 416
    sqlx::query_as(
415 417
        "SELECT suite, classname, name, status, time_ms, message, '' AS output FROM test_cases \
416 -
         WHERE report_id = ? AND status != 'skipped' ORDER BY time_ms DESC, idx LIMIT ?",
418 +
         WHERE report_id = $1 AND status != 'skipped' ORDER BY time_ms DESC, idx LIMIT $2",
417 419
    )
418 420
    .bind(report_id)
419 421
    .bind(limit)
…
4 unchanged lines
424 426
/// Every case of a report as `(classname, name) -> status`.
425 427
pub async fn statuses(pool: &Pool, report_id: i64) -> sqlx::Result<std::collections::HashMap<(String, String), String>> {
426 428
    let rows: Vec<(String, String, String)> =
427 -
        sqlx::query_as("SELECT classname, name, status FROM test_cases WHERE report_id = ?").bind(report_id).fetch_all(pool).await?;
429 +
        sqlx::query_as("SELECT classname, name, status FROM test_cases WHERE report_id = $1").bind(report_id).fetch_all(pool).await?;
428 430
    Ok(rows.into_iter().map(|(c, n, s)| ((c, n), s)).collect())
429 431
}
430 432
…
13 unchanged lines
444 446
    sqlx::query_as(
445 447
        "SELECT r.id AS report_id, r.run_number, r.job, r.created_ms, c.status, c.time_ms, c.message \
446 448
         FROM test_cases c JOIN test_reports r ON r.id = c.report_id \
447 -
         WHERE r.repo_id = ? AND r.bookmark = ? AND c.classname = ? AND c.name = ? \
448 -
         ORDER BY r.created_ms DESC, r.id DESC LIMIT ?",
449 +
         WHERE r.repo_id = $1 AND r.bookmark = $2 AND c.classname = $3 AND c.name = $4 \
450 +
         ORDER BY r.created_ms DESC, r.id DESC LIMIT $5",
449 451
    )
450 452
    .bind(repo_id)
451 453
    .bind(bookmark)
…
8 unchanged lines
460 462
/// the default bookmark for a run on it, its latest for any other.
461 463
pub async fn baseline(pool: &Pool, r: &Report, default_bookmark: &str) -> sqlx::Result<Option<Report>> {
462 464
    sqlx::query_as(
463 -
        "SELECT * FROM test_reports WHERE repo_id = ? AND bookmark = ? AND job = ? AND id != ? \
464 -
         AND (created_ms < ? OR (created_ms = ? AND id < ?)) ORDER BY created_ms DESC, id DESC LIMIT 1",
465 +
        "SELECT * FROM test_reports WHERE repo_id = $1 AND bookmark = $2 AND job = $3 AND id != $4 \
466 +
         AND (created_ms < $5 OR (created_ms = $6 AND id < $7)) ORDER BY created_ms DESC, id DESC LIMIT 1",
465 467
    )
466 468
    .bind(r.repo_id)
467 469
    .bind(default_bookmark)
…
10 unchanged lines
478 480
pub async fn newly_failing(pool: &Pool, report: i64, base: i64) -> sqlx::Result<Vec<String>> {
479 481
    let rows: Vec<(String, String)> = sqlx::query_as(
480 482
        "SELECT c.classname, c.name FROM test_cases c JOIN test_cases b \
481 -
         ON b.report_id = ? AND b.classname = c.classname AND b.name = c.name AND b.status = 'passed' \
482 -
         WHERE c.report_id = ? AND c.status IN ('failed', 'error') ORDER BY c.idx",
483 +
         ON b.report_id = $1 AND b.classname = c.classname AND b.name = c.name AND b.status = 'passed' \
484 +
         WHERE c.report_id = $2 AND c.status IN ('failed', 'error') ORDER BY c.idx",
483 485
    )
484 486
    .bind(base)
485 487
    .bind(report)
…
31 unchanged lines
517 519
    sqlx::query_as(&format!(
518 520
        "SELECT classname, name, SUM(status IN ('failed', 'error')) AS failed, SUM(status = 'passed') AS passed \
519 521
         FROM test_cases WHERE report_id IN ({ids}) GROUP BY classname, name \
520 -
         HAVING failed > 0 AND passed > 0 ORDER BY failed DESC, classname, name LIMIT ?"
522 +
         HAVING failed > 0 AND passed > 0 ORDER BY failed DESC, classname, name LIMIT $1"
521 523
    ))
522 524
    .bind(limit)
523 525
    .fetch_all(pool)
…
56 unchanged lines
modified crates/hub/src/reports.rs +15 −15
…
252 unchanged lines
253 253
/// Index report `name` of a job, replacing any the job kept before.
254 254
pub async fn keep(app: &Shared, o: &Origin<'_>, name: &str, d: &Declaration, m: &Manifest, manifest: &str) -> sqlx::Result<i64> {
255 255
    let mut tx = crate::db::begin_write(&app.db).await?;
256 -
    sqlx::query("DELETE FROM reports WHERE job_id = ? AND name = ?").bind(o.job_id).bind(name).execute(&mut *tx).await?;
256 +
    sqlx::query("DELETE FROM reports WHERE job_id = $1 AND name = $2").bind(o.job_id).bind(name).execute(&mut *tx).await?;
257 257
    let id: i64 = sqlx::query_scalar(
258 258
        "INSERT INTO reports (repo_id, name, kind, title, summary, context, run_id, job_id, run_number, job, bookmark, state, \
259 -
         manifest, files, bytes, created_ms) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
259 +
         manifest, files, bytes, created_ms) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id",
260 260
    )
261 261
    .bind(o.repo_id)
262 262
    .bind(name)
…
15 unchanged lines
278 278
    .await?;
279 279
    for x in &d.metrics {
280 280
        sqlx::query(
281 -
            "INSERT INTO report_metrics (report_id, key, value, unit, better, fail_above, fail_below) VALUES (?, ?, ?, ?, ?, ?, ?)",
281 +
            "INSERT INTO report_metrics (report_id, key, value, unit, better, fail_above, fail_below) VALUES ($1, $2, $3, $4, $5, $6, $7)",
282 282
        )
283 283
        .bind(id)
284 284
        .bind(&x.key)
…
6 unchanged lines
291 291
        .await?;
292 292
    }
293 293
    for f in &m.files {
294 -
        sqlx::query("INSERT INTO report_files (report_id, path, blob, size) VALUES (?, ?, ?, ?)")
294 +
        sqlx::query("INSERT INTO report_files (report_id, path, blob, size) VALUES ($1, $2, $3, $4)")
295 295
            .bind(id)
296 296
            .bind(&f.path)
297 297
            .bind(&f.blob)
…
70 unchanged lines
368 368
/// given (else on any), newest first.
369 369
pub async fn latest(pool: &Pool, repo_id: i64, bookmark: Option<&str>) -> sqlx::Result<Vec<Report>> {
370 370
    sqlx::query_as(
371 -
        "SELECT * FROM reports r WHERE repo_id = ? AND (? IS NULL OR bookmark = ?) \
372 -
         AND id = (SELECT id FROM reports s WHERE s.repo_id = r.repo_id AND s.name = r.name AND (? IS NULL OR s.bookmark = ?) \
371 +
        "SELECT * FROM reports r WHERE repo_id = $1 AND ($2 IS NULL OR bookmark = $3) \
372 +
         AND id = (SELECT id FROM reports s WHERE s.repo_id = r.repo_id AND s.name = r.name AND ($4 IS NULL OR s.bookmark = $5) \
373 373
                   ORDER BY created_ms DESC, id DESC LIMIT 1) \
374 374
         ORDER BY created_ms DESC",
375 375
    )
…
8 unchanged lines
384 384
385 385
/// Report `name`'s history on `bookmark`, newest first.
386 386
pub async fn history(pool: &Pool, repo_id: i64, name: &str, bookmark: &str, limit: i64) -> sqlx::Result<Vec<Report>> {
387 -
    sqlx::query_as("SELECT * FROM reports WHERE repo_id = ? AND name = ? AND bookmark = ? ORDER BY created_ms DESC, id DESC LIMIT ?")
387 +
    sqlx::query_as("SELECT * FROM reports WHERE repo_id = $1 AND name = $2 AND bookmark = $3 ORDER BY created_ms DESC, id DESC LIMIT $4")
388 388
        .bind(repo_id)
389 389
        .bind(name)
390 390
        .bind(bookmark)
…
4 unchanged lines
395 395
396 396
/// The bookmarks report `name` has runs on, most recent first.
397 397
pub async fn bookmarks_of(pool: &Pool, repo_id: i64, name: &str) -> sqlx::Result<Vec<String>> {
398 -
    sqlx::query_scalar("SELECT bookmark FROM reports WHERE repo_id = ? AND name = ? GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
398 +
    sqlx::query_scalar("SELECT bookmark FROM reports WHERE repo_id = $1 AND name = $2 GROUP BY bookmark ORDER BY MAX(created_ms) DESC")
399 399
        .bind(repo_id)
400 400
        .bind(name)
401 401
        .fetch_all(pool)
…
2 unchanged lines
404 404
405 405
/// One report of a repository.
406 406
pub async fn get(pool: &Pool, repo_id: i64, id: i64) -> sqlx::Result<Option<Report>> {
407 -
    sqlx::query_as("SELECT * FROM reports WHERE repo_id = ? AND id = ?").bind(repo_id).bind(id).fetch_optional(pool).await
407 +
    sqlx::query_as("SELECT * FROM reports WHERE repo_id = $1 AND id = $2").bind(repo_id).bind(id).fetch_optional(pool).await
408 408
}
409 409
410 410
/// A file of report `id`: its blob and size.
411 411
pub async fn file(pool: &Pool, id: i64, path: &str) -> sqlx::Result<Option<(String, i64)>> {
412 -
    sqlx::query_as("SELECT blob, size FROM report_files WHERE report_id = ? AND path = ?").bind(id).bind(path).fetch_optional(pool).await
412 +
    sqlx::query_as("SELECT blob, size FROM report_files WHERE report_id = $1 AND path = $2").bind(id).bind(path).fetch_optional(pool).await
413 413
}
414 414
415 415
/// The capability a report's files are served under: they are read from a
…
19 unchanged lines
435 435
        "SELECT r.id FROM reports r JOIN repositories p ON p.id = r.repo_id \
436 436
         WHERE r.files_expired_ms IS NULL AND r.bookmark != p.default_bookmark \
437 437
         AND (SELECT COUNT(*) FROM reports s WHERE s.repo_id = r.repo_id AND s.name = r.name AND s.bookmark = r.bookmark \
438 -
              AND (s.created_ms > r.created_ms OR (s.created_ms = r.created_ms AND s.id > r.id))) >= ?",
438 +
              AND (s.created_ms > r.created_ms OR (s.created_ms = r.created_ms AND s.id > r.id))) >= $1",
439 439
    )
440 440
    .bind(KEEP_OTHER_RUNS)
441 441
    .fetch_all(&mut *tx)
442 442
    .await?;
443 443
    let now = now_ms();
444 444
    for id in &ids {
445 -
        sqlx::query("DELETE FROM report_files WHERE report_id = ?").bind(id).execute(&mut *tx).await?;
446 -
        sqlx::query("UPDATE reports SET files_expired_ms = ? WHERE id = ?").bind(now).bind(id).execute(&mut *tx).await?;
445 +
        sqlx::query("DELETE FROM report_files WHERE report_id = $1").bind(id).execute(&mut *tx).await?;
446 +
        sqlx::query("UPDATE reports SET files_expired_ms = $1 WHERE id = $2").bind(now).bind(id).execute(&mut *tx).await?;
447 447
    }
448 448
    tx.commit().await?;
449 449
    Ok(ids.len() as u64)
…
1 unchanged lines
451 451
452 452
/// Report `id`'s metrics, by key.
453 453
pub async fn metrics(pool: &Pool, id: i64) -> sqlx::Result<Vec<Metric>> {
454 -
    sqlx::query_as("SELECT key, value, unit, better, fail_above, fail_below FROM report_metrics WHERE report_id = ? ORDER BY key")
454 +
    sqlx::query_as("SELECT key, value, unit, better, fail_above, fail_below FROM report_metrics WHERE report_id = $1 ORDER BY key")
455 455
        .bind(id)
456 456
        .fetch_all(pool)
457 457
        .await
…
2 unchanged lines
460 460
/// Every file blob a repository's kept reports name, for garbage collection
461 461
/// to keep too: the database is the only thing that names them.
462 462
pub async fn file_blobs(pool: &Pool, repo_id: i64) -> sqlx::Result<Vec<String>> {
463 -
    sqlx::query_scalar("SELECT DISTINCT f.blob FROM report_files f JOIN reports r ON r.id = f.report_id WHERE r.repo_id = ?")
463 +
    sqlx::query_scalar("SELECT DISTINCT f.blob FROM report_files f JOIN reports r ON r.id = f.report_id WHERE r.repo_id = $1")
464 464
        .bind(repo_id)
465 465
        .fetch_all(pool)
466 466
        .await
…
62 unchanged lines
modified crates/hub/src/secrets.rs +4 −4
…
33 unchanged lines
34 34
}
35 35
36 36
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Secret>> {
37 -
    sqlx::query_as("SELECT * FROM flow_secrets WHERE scope = ? AND scope_id = ? ORDER BY name")
37 +
    sqlx::query_as("SELECT * FROM flow_secrets WHERE scope = $1 AND scope_id = $2 ORDER BY name")
38 38
        .bind(scope.kind())
39 39
        .bind(scope.id())
40 40
        .fetch_all(pool)
…
3 unchanged lines
44 44
/// Set a secret at a level, new or changed; `sealed` is the value sealed.
45 45
pub async fn set(pool: &Pool, scope: Scope, name: &str, sealed: &[u8]) -> sqlx::Result<()> {
46 46
    sqlx::query(
47 -
        "INSERT INTO flow_secrets (scope, scope_id, name, value, updated_ms) VALUES (?, ?, ?, ?, ?) \
47 +
        "INSERT INTO flow_secrets (scope, scope_id, name, value, updated_ms) VALUES ($1, $2, $3, $4, $5) \
48 48
         ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms",
49 49
    )
50 50
    .bind(scope.kind())
…
8 unchanged lines
59 59
60 60
/// Remove a secret from a level; false when it was not there.
61 61
pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result<bool> {
62 -
    let r = sqlx::query("DELETE FROM flow_secrets WHERE scope = ? AND scope_id = ? AND name = ?")
62 +
    let r = sqlx::query("DELETE FROM flow_secrets WHERE scope = $1 AND scope_id = $2 AND name = $3")
63 63
        .bind(scope.kind())
64 64
        .bind(scope.id())
65 65
        .bind(name)
…
4 unchanged lines
70 70
71 71
/// Forget a level's secrets, when the organisation or repository goes.
72 72
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> {
73 -
    sqlx::query("DELETE FROM flow_secrets WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
73 +
    sqlx::query("DELETE FROM flow_secrets WHERE scope = $1 AND scope_id = $2").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
74 74
    Ok(())
75 75
}
76 76
…
87 unchanged lines
modified crates/hub/src/signin.rs +4 −4
…
300 unchanged lines
301 301
}
302 302
303 303
pub async fn linked_account(pool: &Pool, provider: &str, subject: &str) -> sqlx::Result<Option<accounts::Account>> {
304 -
    let id: Option<i64> = sqlx::query_scalar("SELECT account_id FROM account_logins WHERE provider = ? AND subject = ?")
304 +
    let id: Option<i64> = sqlx::query_scalar("SELECT account_id FROM account_logins WHERE provider = $1 AND subject = $2")
305 305
        .bind(provider)
306 306
        .bind(subject)
307 307
        .fetch_optional(pool)
…
5 unchanged lines
313 313
}
314 314
315 315
pub async fn link(pool: &Pool, provider: &str, subject: &str, account_id: i64, login: &str) -> sqlx::Result<()> {
316 -
    sqlx::query("INSERT OR REPLACE INTO account_logins (provider, subject, account_id, login, linked_ms) VALUES (?, ?, ?, ?, ?)")
316 +
    sqlx::query("INSERT OR REPLACE INTO account_logins (provider, subject, account_id, login, linked_ms) VALUES ($1, $2, $3, $4, $5)")
317 317
        .bind(provider)
318 318
        .bind(subject)
319 319
        .bind(account_id)
…
5 unchanged lines
325 325
}
326 326
327 327
pub async fn unlink(pool: &Pool, provider: &str, account_id: i64) -> sqlx::Result<bool> {
328 -
    let r = sqlx::query("DELETE FROM account_logins WHERE provider = ? AND account_id = ?")
328 +
    let r = sqlx::query("DELETE FROM account_logins WHERE provider = $1 AND account_id = $2")
329 329
        .bind(provider)
330 330
        .bind(account_id)
331 331
        .execute(pool)
…
2 unchanged lines
334 334
}
335 335
336 336
pub async fn links_of(pool: &Pool, account_id: i64) -> sqlx::Result<Vec<Login>> {
337 -
    sqlx::query_as("SELECT * FROM account_logins WHERE account_id = ? ORDER BY provider").bind(account_id).fetch_all(pool).await
337 +
    sqlx::query_as("SELECT * FROM account_logins WHERE account_id = $1 ORDER BY provider").bind(account_id).fetch_all(pool).await
338 338
}
339 339
340 340
/// A username for a new account from what the provider calls them:
…
44 unchanged lines
modified crates/hub/src/storage.rs +20 −16
…
121 unchanged lines
122 122
}
123 123
124 124
pub async fn of(pool: &Pool, repo_id: i64) -> sqlx::Result<Option<Storage>> {
125 -
    sqlx::query_as("SELECT * FROM repo_storage WHERE repo_id = ?").bind(repo_id).fetch_optional(pool).await
125 +
    sqlx::query_as("SELECT * FROM repo_storage WHERE repo_id = $1").bind(repo_id).fetch_optional(pool).await
126 126
}
127 127
128 128
/// Check a store URL a user gave: a bucket on S3, Google Cloud Storage or
…
87 unchanged lines
216 216
        return Err(format!("the repository could not be made in {url}: {e}"));
217 217
    }
218 218
    let now = now_ms();
219 -
    sqlx::query("INSERT INTO repo_storage (repo_id, url, credentials, created_ms, checked_ms, check_result) VALUES (?, ?, ?, ?, ?, ?)")
220 -
        .bind(repo_id)
221 -
        .bind(url)
222 -
        .bind(sealed)
223 -
        .bind(now)
224 -
        .bind(now)
225 -
        .bind(&result)
226 -
        .execute(&app.db)
227 -
        .await
228 -
        .map_err(|e| e.to_string())?;
219 +
    sqlx::query(
220 +
        "INSERT INTO repo_storage (repo_id, url, credentials, created_ms, checked_ms, check_result) VALUES ($1, $2, $3, $4, $5, $6)",
221 +
    )
222 +
    .bind(repo_id)
223 +
    .bind(url)
224 +
    .bind(sealed)
225 +
    .bind(now)
226 +
    .bind(now)
227 +
    .bind(&result)
228 +
    .execute(&app.db)
229 +
    .await
230 +
    .map_err(|e| e.to_string())?;
229 231
    Ok(())
230 232
}
231 233
…
6 unchanged lines
238 240
        Ok(r) => r.clone(),
239 241
        Err(e) => format!("failed: {e}"),
240 242
    };
241 -
    sqlx::query("UPDATE repo_storage SET checked_ms = ?, check_result = ? WHERE repo_id = ?")
243 +
    sqlx::query("UPDATE repo_storage SET checked_ms = $1, check_result = $2 WHERE repo_id = $3")
242 244
        .bind(now_ms())
243 245
        .bind(&text)
244 246
        .bind(s.repo_id)
…
7 unchanged lines
252 254
/// check against its store.
253 255
pub async fn set_credentials(app: &Shared, s: &Storage, credentials: Credentials) -> Result<String, String> {
254 256
    let result = probe(&s.url, &credentials, false).await?;
255 -
    sqlx::query("UPDATE repo_storage SET credentials = ?, checked_ms = ?, check_result = ? WHERE repo_id = ?")
257 +
    sqlx::query("UPDATE repo_storage SET credentials = $1, checked_ms = $2, check_result = $3 WHERE repo_id = $4")
256 258
        .bind(app.secret_key.seal(&credentials.to_json()))
257 259
        .bind(now_ms())
258 260
        .bind(&result)
…
50 unchanged lines
309 311
}
310 312
311 313
pub async fn policy(pool: &Pool, org_id: i64) -> sqlx::Result<Policy> {
312 -
    let row: Option<(i64, String)> =
313 -
        sqlx::query_as("SELECT require_bucket, allowed FROM org_storage_policy WHERE org_id = ?").bind(org_id).fetch_optional(pool).await?;
314 +
    let row: Option<(i64, String)> = sqlx::query_as("SELECT require_bucket, allowed FROM org_storage_policy WHERE org_id = $1")
315 +
        .bind(org_id)
316 +
        .fetch_optional(pool)
317 +
        .await?;
314 318
    Ok(row
315 319
        .map(|(r, a)| Policy {
316 320
            require_bucket: r != 0,
…
14 unchanged lines
331 335
        prefixes.push(p.to_string());
332 336
    }
333 337
    sqlx::query(
334 -
        "INSERT INTO org_storage_policy (org_id, require_bucket, allowed) VALUES (?, ?, ?) \
338 +
        "INSERT INTO org_storage_policy (org_id, require_bucket, allowed) VALUES ($1, $2, $3) \
335 339
         ON CONFLICT (org_id) DO UPDATE SET require_bucket = excluded.require_bucket, allowed = excluded.allowed",
336 340
    )
337 341
    .bind(org_id)
…
66 unchanged lines
modified crates/hub/src/variables.rs +4 −4
…
37 unchanged lines
38 38
}
39 39
40 40
pub async fn list(pool: &Pool, scope: Scope) -> sqlx::Result<Vec<Variable>> {
41 -
    sqlx::query_as("SELECT * FROM flow_variables WHERE scope = ? AND scope_id = ? ORDER BY name")
41 +
    sqlx::query_as("SELECT * FROM flow_variables WHERE scope = $1 AND scope_id = $2 ORDER BY name")
42 42
        .bind(scope.kind())
43 43
        .bind(scope.id())
44 44
        .fetch_all(pool)
…
3 unchanged lines
48 48
/// Set a variable at a level, new or changed.
49 49
pub async fn set(pool: &Pool, scope: Scope, name: &str, value: &str) -> sqlx::Result<()> {
50 50
    sqlx::query(
51 -
        "INSERT INTO flow_variables (scope, scope_id, name, value, updated_ms) VALUES (?, ?, ?, ?, ?) \
51 +
        "INSERT INTO flow_variables (scope, scope_id, name, value, updated_ms) VALUES ($1, $2, $3, $4, $5) \
52 52
         ON CONFLICT (scope, scope_id, name) DO UPDATE SET value = excluded.value, updated_ms = excluded.updated_ms",
53 53
    )
54 54
    .bind(scope.kind())
…
8 unchanged lines
63 63
64 64
/// Remove a variable from a level; false when it was not there.
65 65
pub async fn delete(pool: &Pool, scope: Scope, name: &str) -> sqlx::Result<bool> {
66 -
    let r = sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ? AND name = ?")
66 +
    let r = sqlx::query("DELETE FROM flow_variables WHERE scope = $1 AND scope_id = $2 AND name = $3")
67 67
        .bind(scope.kind())
68 68
        .bind(scope.id())
69 69
        .bind(name)
…
4 unchanged lines
74 74
75 75
/// Forget a level's variables, when the organisation or repository goes.
76 76
pub async fn delete_scope(pool: &Pool, scope: Scope) -> sqlx::Result<()> {
77 -
    sqlx::query("DELETE FROM flow_variables WHERE scope = ? AND scope_id = ?").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
77 +
    sqlx::query("DELETE FROM flow_variables WHERE scope = $1 AND scope_id = $2").bind(scope.kind()).bind(scope.id()).execute(pool).await?;
78 78
    Ok(())
79 79
}
80 80
…
50 unchanged lines
modified crates/hub/src/web/inbox.rs +2 −2
…
193 unchanged lines
194 194
    let me = require(&viewer, &shell)?;
195 195
    csrf(&viewer, &f.csrf_token, &shell)?;
196 196
    let key = if me.account.unsubscribe_key.is_empty() { crate::auth::random_hex(16) } else { me.account.unsubscribe_key.clone() };
197 -
    sqlx::query("UPDATE accounts SET email_notifications = ?, unsubscribe_key = ? WHERE id = ?")
197 +
    sqlx::query("UPDATE accounts SET email_notifications = $1, unsubscribe_key = $2 WHERE id = $3")
198 198
        .bind(f.email.is_some())
199 199
        .bind(&key)
200 200
        .bind(me.account.id)
…
18 unchanged lines
219 219
/// The one-click link in every email: no session needed, the key is the proof.
220 220
pub async fn unsubscribe(State(app): State<Shared>, Query(q): Query<UnsubQuery>) -> Response {
221 221
    if let Some(key) = q.unsubscribe.filter(|k| k.len() >= 16) {
222 -
        sqlx::query("UPDATE accounts SET email_notifications = 0 WHERE unsubscribe_key = ?").bind(key).execute(&app.db).await.ok();
222 +
        sqlx::query("UPDATE accounts SET email_notifications = 0 WHERE unsubscribe_key = $1").bind(key).execute(&app.db).await.ok();
223 223
        return (axum::http::StatusCode::OK, "Email notifications are off for that account.").into_response();
224 224
    }
225 225
    Redirect::to("/notifications").into_response()
…
77 unchanged lines
modified crates/hub/src/web/issues.rs +1 −1
…
133 unchanged lines
134 134
            && f.milestone.as_ref().is_none_or(|m| milestone.as_ref().is_some_and(|x| x.to_lowercase() == *m))
135 135
            && f.words.iter().all(|w| i.title.to_lowercase().contains(w) || i.body.to_lowercase().contains(w));
136 136
        if keep {
137 -
            let comments: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issue_comments WHERE issue_id = ?")
137 +
            let comments: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM issue_comments WHERE issue_id = $1")
138 138
                .bind(i.id)
139 139
                .fetch_one(&app.db)
140 140
                .await
…
941 unchanged lines
modified crates/hub/src/web/projects.rs +1 −1
…
403 unchanged lines
404 404
}
405 405
406 406
async fn newest_card(app: &Shared, project_id: i64) -> sqlx::Result<pdb::Card> {
407 -
    sqlx::query_as("SELECT * FROM project_cards WHERE project_id = ? ORDER BY id DESC LIMIT 1").bind(project_id).fetch_one(&app.db).await
407 +
    sqlx::query_as("SELECT * FROM project_cards WHERE project_id = $1 ORDER BY id DESC LIMIT 1").bind(project_id).fetch_one(&app.db).await
408 408
}
409 409
410 410
pub async fn act(
…
251 unchanged lines
modified crates/hub/src/web/proposals.rs +2 −2
…
546 unchanged lines
547 547
                // The target moved, so the proposal's projected state is
548 548
                // new and its checks would be for an older one (#134): run
549 549
                // the pipeline on it, once.
550 -
                let ran: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM flow_runs WHERE repo_id = ? AND state = ?")
550 +
                let ran: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM flow_runs WHERE repo_id = $1 AND state = $2")
551 551
                    .bind(row.id)
552 552
                    .bind(&l.state)
553 553
                    .fetch_one(&app.db)
…
562 unchanged lines
1116 1116
        .await
1117 1117
        .map_err(|e| Error::from_db(shell.clone(), e))?
1118 1118
        .ok_or_else(|| Error::not_found(shell.clone()))?;
1119 -
    let current: Option<bool> = sqlx::query_scalar("SELECT resolved FROM proposal_comments WHERE id = ? AND proposal_id = ?")
1119 +
    let current: Option<bool> = sqlx::query_scalar("SELECT resolved FROM proposal_comments WHERE id = $1 AND proposal_id = $2")
1120 1120
        .bind(id)
1121 1121
        .bind(p.id)
1122 1122
        .fetch_optional(&app.db)
…
640 unchanged lines
modified crates/hub/src/web/releases.rs +1 −1
…
399 unchanged lines
400 400
    let (_, row, _) = lookup(&app, &shell, &viewer, &owner, &name).await?;
401 401
    let e = |e| Error::from_db(shell.clone(), e);
402 402
    let asset: Option<rdb::Asset> =
403 -
        sqlx::query_as("SELECT a.* FROM release_assets a JOIN releases r ON r.id = a.release_id WHERE a.id = ? AND r.repo_id = ?")
403 +
        sqlx::query_as("SELECT a.* FROM release_assets a JOIN releases r ON r.id = a.release_id WHERE a.id = $1 AND r.repo_id = $2")
404 404
            .bind(id)
405 405
            .bind(row.id)
406 406
            .fetch_optional(&app.db)
…
111 unchanged lines
modified crates/hub/src/web/settings.rs +2 −2
…
587 unchanged lines
588 588
    }
589 589
    let bytes = auth::random_hex(32);
590 590
    let id = accounts::create_challenge(&app.db, me.account.id, &bytes).await.map_err(|e| Error::from_db(shell.clone(), e))?;
591 -
    sqlx::query("UPDATE challenges SET object = ? WHERE id = ?")
591 +
    sqlx::query("UPDATE challenges SET object = $1 WHERE id = $2")
592 592
        .bind(&object)
593 593
        .bind(id)
594 594
        .execute(&app.db)
…
14 unchanged lines
609 609
    let shell = Shell::new(&app, "/settings/keys", &viewer);
610 610
    let me = require(&viewer, &shell)?;
611 611
    csrf(&viewer, &f.csrf_token, &shell)?;
612 -
    let object: Option<Vec<u8>> = sqlx::query_scalar("SELECT object FROM challenges WHERE id = ? AND account_id = ?")
612 +
    let object: Option<Vec<u8>> = sqlx::query_scalar("SELECT object FROM challenges WHERE id = $1 AND account_id = $2")
613 613
        .bind(f.challenge)
614 614
        .bind(me.account.id)
615 615
        .fetch_optional(&app.db)
…
198 unchanged lines