uqa_storage/
statistics_maintenance.rs1use crate::{CatalogFacade, StorageBackendResult};
10use serde::{Deserialize, Serialize};
11
12const MAX_DIRTY_AGE_MS: u64 = 60_000;
13const MIN_KEPT_CHANGES: u64 = 16;
15
16const fn change_threshold(analyzed_rows: u64) -> u64 {
18 50 + analyzed_rows / 10
19}
20
21#[cfg(test)]
22mod tests;
23
24#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(default)]
28pub struct StatisticsMaintenance {
29 object_id: Option<[u8; 16]>,
30 generation: u64,
31 changes: u64,
32 dirty_since_ms: u64,
33 analyzed_rows: Option<u64>,
34 statistics_format: u32,
35 #[serde(skip_serializing_if = "Option::is_none")]
37 analyzed_at: Option<u64>,
38 #[serde(skip_serializing_if = "Option::is_none")]
41 analysis_generation: Option<u64>,
42}
43
44impl StatisticsMaintenance {
45 pub const KEY_PREFIX: &'static str = "uqa.statistics.maintenance.v1:";
46
47 pub fn key(table: &str) -> String {
48 format!("{}{table}", Self::KEY_PREFIX)
49 }
50
51 pub fn load(catalog: &dyn CatalogFacade, table: &str) -> StorageBackendResult<Self> {
52 catalog
53 .get_metadata(&Self::key(table))?
54 .map(|json| serde_json::from_str(&json).map_err(Into::into))
55 .transpose()
56 .map(Option::unwrap_or_default)
57 }
58
59 pub fn load_for(
60 catalog: &dyn CatalogFacade,
61 table: &str,
62 object_id: [u8; 16],
63 ) -> StorageBackendResult<Self> {
64 let state = Self::load(catalog, table)?;
65 Ok(
66 if state.object_id.is_some_and(|stored| stored != object_id) {
67 Self::default()
68 } else {
69 state
70 },
71 )
72 }
73
74 pub fn save(&self, catalog: &dyn CatalogFacade, table: &str) -> StorageBackendResult<()> {
75 catalog.save_statistics_maintenance(table, self)
76 }
77
78 pub fn dirty(&self) -> bool {
79 self.changes != 0
80 }
81
82 pub fn missing(&self, statistics_empty: bool) -> bool {
83 self.analyzed_rows.is_none() && statistics_empty
84 }
85
86 pub fn invalidates_existing_statistics(&self) -> bool {
87 self.dirty() && self.analyzed_rows.is_some()
90 }
91
92 pub fn due(&self, missing: bool, now: u64, statistics_format: u32) -> bool {
93 self.statistics_format != statistics_format
94 || missing
95 || (self.dirty()
96 && (self.analyzed_rows == Some(0)
97 || self.changes >= change_threshold(self.analyzed_rows.unwrap_or(0))
98 || now.saturating_sub(self.dirty_since_ms) >= MAX_DIRTY_AGE_MS))
99 }
100
101 pub fn defers(&self, unrecorded: u64, now: u64) -> bool {
103 if !self.dirty() {
104 return false;
105 }
106 let Some(analyzed_rows) = self.analyzed_rows else {
107 return true;
109 };
110 let threshold = change_threshold(analyzed_rows);
111 if analyzed_rows == 0
112 || self.changes >= threshold
113 || now.saturating_sub(self.dirty_since_ms) >= MAX_DIRTY_AGE_MS
114 {
115 return true;
116 }
117 self.changes.saturating_add(unrecorded) < threshold
118 && unrecorded < (threshold / 4).max(MIN_KEPT_CHANGES)
119 }
120
121 pub fn analyzed_for(
122 catalog: &dyn CatalogFacade,
123 table: &str,
124 object_id: [u8; 16],
125 rows: u64,
126 statistics_format: u32,
127 sampled_at: Option<u64>,
128 ) -> StorageBackendResult<()> {
129 let mut state = Self::load_for(catalog, table, object_id)?;
130 state.analyzed(object_id, rows, statistics_format, sampled_at)?;
131 state.save(catalog, table)
132 }
133
134 pub fn analyzed(
136 &mut self,
137 object_id: [u8; 16],
138 rows: u64,
139 statistics_format: u32,
140 sampled_at: Option<u64>,
141 ) -> StorageBackendResult<()> {
142 self.object_id = Some(object_id);
143 self.advance_generation()?;
144 self.analysis_generation = Some(self.generation);
145 self.changes = 0;
146 self.dirty_since_ms = 0;
147 self.analyzed_rows = Some(rows);
148 self.statistics_format = statistics_format;
149 self.analyzed_at = sampled_at;
150 Ok(())
151 }
152
153 pub fn covers(&self, through: Option<u64>) -> bool {
155 matches!((self.analyzed_at, through), (Some(at), Some(through)) if through <= at)
156 }
157
158 pub fn same_analysis(&self, other: &Self) -> bool {
161 self.analysis_generation == other.analysis_generation
162 && self.analyzed_at == other.analyzed_at
163 && self.analyzed_rows == other.analyzed_rows
164 && self.statistics_format == other.statistics_format
165 }
166
167 pub fn merge_sample(
174 &self,
175 before: &Self,
176 current: &Self,
177 data_changed: bool,
178 now: u64,
179 ) -> StorageBackendResult<Option<Self>> {
180 let Some(mut merged) = Self::merge(before, self, current)
181 .map_err(crate::mvcc::VersionError::into_storage_error)?
182 else {
183 return Ok(None);
184 };
185 if data_changed && !merged.dirty() {
186 merged.changes = 1;
187 merged.dirty_since_ms = now;
188 }
189 Ok(Some(merged))
190 }
191
192 fn advance_generation(&mut self) -> StorageBackendResult<()> {
193 self.generation = self.generation.checked_add(1).ok_or_else(|| {
194 crate::StorageBackendError::Other("statistics generation space exhausted".into())
195 })?;
196 Ok(())
197 }
198
199 pub fn record_changes(
200 &mut self,
201 object_id: [u8; 16],
202 count: u64,
203 previous_rows: Option<u64>,
204 now: u64,
205 ) -> StorageBackendResult<()> {
206 self.object_id = Some(object_id);
207 self.advance_generation()?;
208 if self.analyzed_rows.is_none() {
209 self.analyzed_rows = previous_rows;
210 }
211 if !self.dirty() {
212 self.dirty_since_ms = now;
213 }
214 self.changes = self.changes.checked_add(count).ok_or_else(|| {
215 crate::StorageBackendError::Other("statistics change count overflow".into())
216 })?;
217 Ok(())
218 }
219
220 pub fn encode(
222 &self,
223 control: &crate::read_control::StorageReadControl,
224 ) -> crate::mvcc::VersionResult<uqa_core::memory::BudgetedVec<u8>> {
225 crate::key_value::record_json::encode(self, control)
226 }
227
228 pub(crate) fn merge(
230 before: &Self,
231 after: &Self,
232 current: &Self,
233 ) -> crate::mvcc::VersionResult<Option<Self>> {
234 use crate::mvcc::VersionError;
235 if before
236 .object_id
237 .is_some_and(|id| Some(id) != after.object_id)
238 || current
239 .object_id
240 .is_some_and(|id| Some(id) != after.object_id)
241 || after.generation < before.generation
242 {
243 return Ok(None);
244 }
245 let changes =
246 i128::from(current.changes) + i128::from(after.changes) - i128::from(before.changes);
247 if changes < 0 {
248 return Ok(None);
249 }
250 let changes = u64::try_from(changes)
251 .map_err(|_| VersionError::InvalidEncoding("statistics change count overflow"))?;
252 let generation = current
253 .generation
254 .checked_add(after.generation - before.generation)
255 .ok_or(VersionError::InvalidEncoding(
256 "statistics generation space exhausted",
257 ))?;
258 let changed_estimate = after.statistics_format != 0
259 && (after.analyzed_rows != before.analyzed_rows
260 || after.statistics_format != before.statistics_format);
261 Ok(Some(Self {
262 object_id: after.object_id,
263 generation,
264 changes,
265 dirty_since_ms: if changes == 0 {
266 0
267 } else {
268 [after.dirty_since_ms, current.dirty_since_ms]
269 .into_iter()
270 .filter(|time| *time != 0)
271 .min()
272 .unwrap_or(0)
273 },
274 analyzed_rows: if changed_estimate {
275 after.analyzed_rows
276 } else {
277 current.analyzed_rows.or(after.analyzed_rows)
278 },
279 statistics_format: if changed_estimate {
280 after.statistics_format
281 } else {
282 current.statistics_format
283 },
284 analyzed_at: if after.analyzed_at == before.analyzed_at {
286 current.analyzed_at
287 } else {
288 after.analyzed_at
289 },
290 analysis_generation: if after.analysis_generation == before.analysis_generation {
291 current.analysis_generation
292 } else {
293 after.analysis_generation
294 },
295 }))
296 }
297}