1use anyhow::Result;
2use rusqlite::{Connection, OptionalExtension};
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct LegacySurfaceStats {
6 pub surface: String,
7 pub disposition: String,
8 pub row_count: i64,
9 pub last_write_epoch: Option<i64>,
10 pub frozen_write_violations: i64,
11}
12
13pub(super) fn query_legacy_surface_stats(conn: &Connection) -> Result<Vec<LegacySurfaceStats>> {
14 let observations = legacy_table_surface(conn, "observations", "reclassify-current", &[])?;
15 let observations_fts =
16 legacy_table_surface(conn, "observations_fts", "reclassify-current", &[])?;
17 let session_summaries = legacy_table_surface(conn, "session_summaries", "keep", &[])?;
18 let pending_observations = legacy_pending_observation_surface(conn)?;
19 let summary_jobs = legacy_summary_job_surface(conn)?;
20
21 Ok(vec![
22 observations,
23 observations_fts,
24 session_summaries,
25 pending_observations,
26 summary_jobs,
27 ])
28}
29
30fn legacy_table_surface(
31 conn: &Connection,
32 table: &str,
33 disposition: &str,
34 violation_epoch_columns: &[&str],
35) -> Result<LegacySurfaceStats> {
36 let row_count = table_count_or_zero(conn, table)?;
37 let last_write_epoch = max_write_epoch(conn, table)?;
38 let mut has_violation_epoch = false;
39 for column in violation_epoch_columns {
40 has_violation_epoch |= column_exists(conn, table, column)?;
41 }
42 let frozen_write_violations = if row_count > 0 && has_violation_epoch {
43 row_count
44 } else {
45 0
46 };
47 Ok(LegacySurfaceStats {
48 surface: table.to_string(),
49 disposition: disposition.to_string(),
50 row_count,
51 last_write_epoch,
52 frozen_write_violations,
53 })
54}
55
56fn legacy_pending_observation_surface(conn: &Connection) -> Result<LegacySurfaceStats> {
57 let table = "pending_observations";
58 let row_count = table_count_or_zero(conn, table)?;
59 let last_write_epoch = max_write_epoch(conn, table)?;
60 let frozen_write_violations = if !table_exists(conn, table)? {
61 0
62 } else if column_exists(conn, table, "status")? {
63 let archived_filter = if column_exists(conn, table, "archived_at_epoch")? {
64 "AND archived_at_epoch IS NULL"
65 } else {
66 ""
67 };
68 conn.query_row(
69 &format!(
70 "SELECT COUNT(*) FROM pending_observations
71 WHERE status <> 'migrated'
72 {archived_filter}"
73 ),
74 [],
75 |row| row.get(0),
76 )?
77 } else {
78 row_count
79 };
80
81 Ok(LegacySurfaceStats {
82 surface: table.to_string(),
83 disposition: "retire".to_string(),
84 row_count,
85 last_write_epoch,
86 frozen_write_violations,
87 })
88}
89
90fn legacy_summary_job_surface(conn: &Connection) -> Result<LegacySurfaceStats> {
91 let surface = "summary_jobs".to_string();
92 let disposition = "retire-summary-only".to_string();
93 if !table_exists(conn, "jobs")? || !column_exists(conn, "jobs", "job_type")? {
94 return Ok(LegacySurfaceStats {
95 surface,
96 disposition,
97 row_count: 0,
98 last_write_epoch: None,
99 frozen_write_violations: 0,
100 });
101 }
102
103 let row_count = conn.query_row(
104 "SELECT COUNT(*) FROM jobs WHERE job_type = 'summary'",
105 [],
106 |row| row.get(0),
107 )?;
108 let frozen_write_violations = if column_exists(conn, "jobs", "state")? {
109 let archived_filter = if column_exists(conn, "jobs", "archived_at_epoch")? {
110 "AND archived_at_epoch IS NULL"
111 } else {
112 ""
113 };
114 let rejection_filter = if column_exists(conn, "jobs", "failure_class")?
115 && column_exists(conn, "jobs", "last_error")?
116 {
117 "AND NOT (
118 state = 'failed'
119 AND failure_class = 'permanent'
120 AND last_error = 'legacy summary job rejected during GH684 summary retirement upgrade; SessionRollup owns session summary output'
121 )"
122 } else {
123 ""
124 };
125 conn.query_row(
126 &format!(
127 "SELECT COUNT(*) FROM jobs
128 WHERE job_type = 'summary'
129 AND state <> 'done'
130 {rejection_filter}
131 {archived_filter}"
132 ),
133 [],
134 |row| row.get(0),
135 )?
136 } else {
137 row_count
138 };
139 let last_write_epoch = max_write_epoch_where(conn, "jobs", "job_type = 'summary'")?;
140 Ok(LegacySurfaceStats {
141 surface,
142 disposition,
143 row_count,
144 last_write_epoch,
145 frozen_write_violations,
146 })
147}
148
149fn table_exists(conn: &Connection, table: &str) -> Result<bool> {
150 Ok(conn
151 .query_row(
152 "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?1",
153 [table],
154 |_| Ok(()),
155 )
156 .optional()?
157 .is_some())
158}
159
160fn table_count_or_zero(conn: &Connection, table: &str) -> Result<i64> {
161 if !table_exists(conn, table)? {
162 return Ok(0);
163 }
164 Ok(conn.query_row(
165 &format!("SELECT COUNT(*) FROM {}", quote_identifier(table)),
166 [],
167 |row| row.get(0),
168 )?)
169}
170
171fn max_write_epoch(conn: &Connection, table: &str) -> Result<Option<i64>> {
172 max_write_epoch_where(conn, table, "1 = 1")
173}
174
175fn max_write_epoch_where(
176 conn: &Connection,
177 table: &str,
178 where_clause: &str,
179) -> Result<Option<i64>> {
180 if !table_exists(conn, table)? {
181 return Ok(None);
182 }
183 let mut columns = Vec::new();
184 if column_exists(conn, table, "updated_at_epoch")? {
185 columns.push("updated_at_epoch");
186 }
187 if column_exists(conn, table, "created_at_epoch")? {
188 columns.push("created_at_epoch");
189 }
190 if columns.is_empty() {
191 return Ok(None);
192 }
193
194 let expression = match columns.as_slice() {
195 ["updated_at_epoch", "created_at_epoch"] => {
196 "CASE
197 WHEN NULLIF(updated_at_epoch, 0) IS NULL THEN NULLIF(created_at_epoch, 0)
198 WHEN NULLIF(created_at_epoch, 0) IS NULL THEN NULLIF(updated_at_epoch, 0)
199 WHEN updated_at_epoch >= created_at_epoch THEN updated_at_epoch
200 ELSE created_at_epoch
201 END"
202 }
203 [single] => match *single {
204 "updated_at_epoch" => "NULLIF(updated_at_epoch, 0)",
205 "created_at_epoch" => "NULLIF(created_at_epoch, 0)",
206 _ => unreachable!("legacy write epoch columns are fixed"),
207 },
208 _ => unreachable!("legacy write epoch columns are fixed"),
209 };
210 let sql = format!(
211 "SELECT MAX({expression}) FROM {} WHERE {where_clause}",
212 quote_identifier(table)
213 );
214 Ok(conn.query_row(&sql, [], |row| row.get(0))?)
215}
216
217fn column_exists(conn: &Connection, table: &str, column: &str) -> Result<bool> {
218 if !table_exists(conn, table)? {
219 return Ok(false);
220 }
221 let mut stmt = conn.prepare(&format!("PRAGMA table_info({})", quote_identifier(table)))?;
222 let mut rows = stmt.query([])?;
223 while let Some(row) = rows.next()? {
224 let name: String = row.get(1)?;
225 if name == column {
226 return Ok(true);
227 }
228 }
229 Ok(false)
230}
231
232fn quote_identifier(identifier: &str) -> String {
233 format!("\"{}\"", identifier.replace('"', "\"\""))
234}