1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//! Project-level aggregate queries on `Database` (list / count).
//!
//! Split out of `mod.rs` to keep that file under the project 500-line cap;
//! these are the project-scan surface the doctor and prune handlers drive,
//! and need nothing but the read-only [`Database::conn`] handle.
use super::{Database, Result};
impl Database {
/// List all distinct project_ids in the database.
///
/// Callers that need counts as well should drive
/// [`Database::row_counts_by_project`] and read the keys off its result:
/// one `GROUP BY` pass covers both, whereas this query plus that one is
/// two passes over `memories`.
///
/// # Errors
///
/// Returns error if the database query fails.
pub fn list_all_project_ids(&self) -> Result<Vec<String>> {
let mut stmt = self
.conn()
.prepare("SELECT DISTINCT project_id FROM memories ORDER BY project_id")?;
let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
let mut results = Vec::new();
for row_result in rows {
results.push(row_result?);
}
Ok(results)
}
/// Row count for EVERY project, in one `GROUP BY` pass.
///
/// The project-scan helper the doctor handlers use. A `COUNT(*)` per
/// project id costs one query per project; the `doctor --projects` and
/// `doctor --fts` scans drive this instead of looping (the same
/// don't-drive-it-from-a-loop shape `export_scan::scan_all_rows` uses).
///
/// **Contract: a project with zero `memories` rows is absent from the
/// result** — `GROUP BY` only emits groups that exist. Callers that must
/// report such a project read it from elsewhere (`list_all_project_ids`)
/// or treat a missing key as zero; `build_response` in
/// `commands/doctor_fts.rs` and `detect_split_pairs` in
/// `commands/doctor.rs` both do the latter. Pinned by
/// `sqlite::tests::row_counts_by_project_omits_zero_row_projects`.
///
/// `INDEXED BY idx_memories_project` pins the covering-index path: the
/// aggregate reads only `project_id`, so the scan of that index is all
/// SQLite needs, and the hint stops the planner from choosing a full-table
/// scan instead. `idx_memories_project` is created by `create_schema`, so
/// it exists on every database this method can be called on.
///
/// # Errors
///
/// Returns error if the database query fails.
pub fn row_counts_by_project(&self) -> Result<Vec<(String, usize)>> {
let mut stmt = self.conn().prepare(
"SELECT project_id, COUNT(*) FROM memories \
INDEXED BY idx_memories_project GROUP BY project_id",
)?;
let rows = stmt.query_map([], |row| {
// `isize`, not `i64` + `as usize`: rusqlite has no `FromSql` for
// `usize` (see its impl list), and `COUNT(*)` is an SQL integer
// that `isize` holds exactly, so the widening conversion is the
// one that cannot silently truncate a value the caller counts on.
Ok((row.get::<_, String>(0)?, row.get::<_, isize>(1)? as usize))
})?;
let mut results = Vec::new();
for row_result in rows {
results.push(row_result?);
}
Ok(results)
}
}