Skip to main content

max / balanced_breakfast

2.3 KB · 80 lines History Blame Raw
1 //! Small helpers the repositories share.
2 //!
3 //! These exist so a repository method reads as one line each, and so the
4 //! [`DbError`] mapping lives in one place instead of on every `?`.
5
6 use rusqlite::{Connection, Params, Row};
7
8 use crate::error::DbError;
9
10 /// Fetch every matching row, mapped by `f`.
11 pub fn query_all<T, P, F>(conn: &Connection, sql: &str, params: P, f: F) -> Result<Vec<T>, DbError>
12 where
13 P: Params,
14 F: FnMut(&Row<'_>) -> rusqlite::Result<T>,
15 {
16 let mut stmt = conn.prepare(sql)?;
17 let rows = stmt.query_map(params, f)?;
18 Ok(rows.collect::<rusqlite::Result<Vec<T>>>()?)
19 }
20
21 /// Fetch at most one row, mapped by `f`. `None` when nothing matched.
22 pub fn query_opt<T, P, F>(
23 conn: &Connection,
24 sql: &str,
25 params: P,
26 f: F,
27 ) -> Result<Option<T>, DbError>
28 where
29 P: Params,
30 F: FnOnce(&Row<'_>) -> rusqlite::Result<T>,
31 {
32 use rusqlite::OptionalExtension as _;
33 Ok(conn.query_row(sql, params, f).optional()?)
34 }
35
36 /// Fetch exactly one row, mapped by `f`.
37 ///
38 /// For queries that always produce a row -- aggregates, and the `RETURNING *`
39 /// on an insert -- where no rows means something is wrong rather than absent.
40 pub fn query_one<T, P, F>(conn: &Connection, sql: &str, params: P, f: F) -> Result<T, DbError>
41 where
42 P: Params,
43 F: FnOnce(&Row<'_>) -> rusqlite::Result<T>,
44 {
45 Ok(conn.query_row(sql, params, f)?)
46 }
47
48 /// Run a statement, returning the number of rows it changed.
49 ///
50 /// The return value is the number of rows affected.
51 pub fn execute<P: Params>(conn: &Connection, sql: &str, params: P) -> Result<usize, DbError> {
52 Ok(conn.execute(sql, params)?)
53 }
54
55 /// Build a comma-separated list of `n` numbered bind placeholders
56 /// (`?1,?2,...,?n`) for an `IN (...)` clause.
57 ///
58 /// Callers guard the empty case before building the query (an empty `IN ()` is
59 /// invalid SQL); this returns `""` for `n == 0` so the guard stays their
60 /// responsibility.
61 #[must_use]
62 pub fn bind_placeholders(n: usize) -> String {
63 (1..=n)
64 .map(|i| format!("?{i}"))
65 .collect::<Vec<_>>()
66 .join(", ")
67 }
68
69 #[cfg(test)]
70 mod tests {
71 use super::*;
72
73 #[test]
74 fn placeholders_are_numbered_from_one() {
75 assert_eq!(bind_placeholders(0), "");
76 assert_eq!(bind_placeholders(1), "?1");
77 assert_eq!(bind_placeholders(3), "?1, ?2, ?3");
78 }
79 }
80