Skip to main content

kaizen/store/sqlite/
metrics.rs

1use super::rows::*;
2use super::*;
3impl Store {
4    pub fn latest_repo_snapshot(&self, workspace: &str) -> Result<Option<RepoSnapshotRecord>> {
5        let mut stmt = self.conn.prepare(
6            "SELECT id, workspace, head_commit, dirty_fingerprint, analyzer_version,
7                    indexed_at_ms, dirty, graph_path
8             FROM repo_snapshots WHERE workspace = ?1
9             ORDER BY indexed_at_ms DESC LIMIT 1",
10        )?;
11        let mut rows = stmt.query_map(params![workspace], |row| {
12            Ok(RepoSnapshotRecord {
13                id: row.get(0)?,
14                workspace: row.get(1)?,
15                head_commit: row.get(2)?,
16                dirty_fingerprint: row.get(3)?,
17                analyzer_version: row.get(4)?,
18                indexed_at_ms: row.get::<_, i64>(5)? as u64,
19                dirty: row.get::<_, i64>(6)? != 0,
20                graph_path: row.get(7)?,
21            })
22        })?;
23        Ok(rows.next().transpose()?)
24    }
25
26    pub fn save_repo_snapshot(
27        &self,
28        snapshot: &RepoSnapshotRecord,
29        facts: &[FileFact],
30        edges: &[RepoEdge],
31    ) -> Result<()> {
32        self.conn.execute(
33            "INSERT INTO repo_snapshots (
34                id, workspace, head_commit, dirty_fingerprint, analyzer_version,
35                indexed_at_ms, dirty, graph_path
36             ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
37             ON CONFLICT(id) DO UPDATE SET
38                workspace=excluded.workspace,
39                head_commit=excluded.head_commit,
40                dirty_fingerprint=excluded.dirty_fingerprint,
41                analyzer_version=excluded.analyzer_version,
42                indexed_at_ms=excluded.indexed_at_ms,
43                dirty=excluded.dirty,
44                graph_path=excluded.graph_path",
45            params![
46                snapshot.id,
47                snapshot.workspace,
48                snapshot.head_commit,
49                snapshot.dirty_fingerprint,
50                snapshot.analyzer_version,
51                snapshot.indexed_at_ms as i64,
52                bool_to_i64(snapshot.dirty),
53                snapshot.graph_path,
54            ],
55        )?;
56        self.conn.execute(
57            "DELETE FROM file_facts WHERE snapshot_id = ?1",
58            params![snapshot.id],
59        )?;
60        self.conn.execute(
61            "DELETE FROM repo_edges WHERE snapshot_id = ?1",
62            params![snapshot.id],
63        )?;
64        for fact in facts {
65            self.conn.execute(
66                "INSERT INTO file_facts (
67                    snapshot_id, path, language, bytes, loc, sloc, complexity_total,
68                    max_fn_complexity, symbol_count, import_count, fan_in, fan_out,
69                    churn_30d, churn_90d, authors_90d, last_changed_ms
70                 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16)",
71                params![
72                    fact.snapshot_id,
73                    fact.path,
74                    fact.language,
75                    fact.bytes as i64,
76                    fact.loc as i64,
77                    fact.sloc as i64,
78                    fact.complexity_total as i64,
79                    fact.max_fn_complexity as i64,
80                    fact.symbol_count as i64,
81                    fact.import_count as i64,
82                    fact.fan_in as i64,
83                    fact.fan_out as i64,
84                    fact.churn_30d as i64,
85                    fact.churn_90d as i64,
86                    fact.authors_90d as i64,
87                    fact.last_changed_ms.map(|v| v as i64),
88                ],
89            )?;
90        }
91        for edge in edges {
92            self.conn.execute(
93                "INSERT INTO repo_edges (snapshot_id, from_id, to_id, kind, weight)
94                 VALUES (?1, ?2, ?3, ?4, ?5)
95                 ON CONFLICT(snapshot_id, from_id, to_id, kind)
96                 DO UPDATE SET weight = weight + excluded.weight",
97                params![
98                    snapshot.id,
99                    edge.from_path,
100                    edge.to_path,
101                    edge.kind,
102                    edge.weight as i64,
103                ],
104            )?;
105        }
106        Ok(())
107    }
108
109    pub fn file_facts_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<FileFact>> {
110        let mut stmt = self.conn.prepare(
111            "SELECT snapshot_id, path, language, bytes, loc, sloc, complexity_total,
112                    max_fn_complexity, symbol_count, import_count, fan_in, fan_out,
113                    churn_30d, churn_90d, authors_90d, last_changed_ms
114             FROM file_facts WHERE snapshot_id = ?1 ORDER BY path ASC",
115        )?;
116        let rows = stmt.query_map(params![snapshot_id], |row| {
117            Ok(FileFact {
118                snapshot_id: row.get(0)?,
119                path: row.get(1)?,
120                language: row.get(2)?,
121                bytes: row.get::<_, i64>(3)? as u64,
122                loc: row.get::<_, i64>(4)? as u32,
123                sloc: row.get::<_, i64>(5)? as u32,
124                complexity_total: row.get::<_, i64>(6)? as u32,
125                max_fn_complexity: row.get::<_, i64>(7)? as u32,
126                symbol_count: row.get::<_, i64>(8)? as u32,
127                import_count: row.get::<_, i64>(9)? as u32,
128                fan_in: row.get::<_, i64>(10)? as u32,
129                fan_out: row.get::<_, i64>(11)? as u32,
130                churn_30d: row.get::<_, i64>(12)? as u32,
131                churn_90d: row.get::<_, i64>(13)? as u32,
132                authors_90d: row.get::<_, i64>(14)? as u32,
133                last_changed_ms: row.get::<_, Option<i64>>(15)?.map(|v| v as u64),
134            })
135        })?;
136        Ok(rows.filter_map(|row| row.ok()).collect())
137    }
138    pub fn repo_edges_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<RepoEdge>> {
139        let mut stmt = self.conn.prepare(
140            "SELECT from_id, to_id, kind, weight
141             FROM repo_edges WHERE snapshot_id = ?1
142             ORDER BY kind, from_id, to_id",
143        )?;
144        let rows = stmt.query_map(params![snapshot_id], |row| {
145            Ok(RepoEdge {
146                from_path: row.get(0)?,
147                to_path: row.get(1)?,
148                kind: row.get(2)?,
149                weight: row.get::<_, i64>(3)? as u32,
150            })
151        })?;
152        Ok(rows.filter_map(|row| row.ok()).collect())
153    }
154    pub fn hottest_files_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<RankedFile>> {
155        self.ranked_files_for_snapshot(snapshot_id, "churn_30d * complexity_total")
156    }
157
158    pub fn most_changed_files_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<RankedFile>> {
159        self.ranked_files_for_snapshot(snapshot_id, "churn_30d")
160    }
161
162    pub fn most_complex_files_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<RankedFile>> {
163        self.ranked_files_for_snapshot(snapshot_id, "complexity_total")
164    }
165
166    pub fn highest_risk_files_for_snapshot(&self, snapshot_id: &str) -> Result<Vec<RankedFile>> {
167        self.ranked_files_for_snapshot(snapshot_id, "churn_30d * authors_90d * complexity_total")
168    }
169
170    pub(super) fn ranked_files_for_snapshot(
171        &self,
172        snapshot_id: &str,
173        value_sql: &str,
174    ) -> Result<Vec<RankedFile>> {
175        let sql = format!(
176            "SELECT path, {value_sql}, complexity_total, churn_30d
177             FROM file_facts WHERE snapshot_id = ?1
178             ORDER BY {value_sql} DESC, path ASC LIMIT 10"
179        );
180        let mut stmt = self.conn.prepare(&sql)?;
181        let rows = stmt.query_map(params![snapshot_id], ranked_file_row)?;
182        rows.map(|r| r.map_err(anyhow::Error::from)).collect()
183    }
184
185    pub fn pain_hotspots_for_snapshot(
186        &self,
187        snapshot_id: &str,
188        workspace: &str,
189        start_ms: u64,
190        end_ms: u64,
191    ) -> Result<Vec<RankedFile>> {
192        let mut stmt = self.conn.prepare(PAIN_HOTSPOTS_SQL)?;
193        let rows = stmt.query_map(
194            params![snapshot_id, workspace, start_ms as i64, end_ms as i64],
195            ranked_file_row,
196        )?;
197        rows.map(|r| r.map_err(anyhow::Error::from)).collect()
198    }
199}