uqa_storage/
statistics_maintenance.rs1use crate::{CatalogFacade, StorageBackendResult};
10use serde::{Deserialize, Serialize};
11
12const MAX_DIRTY_AGE_MS: u64 = 60_000;
13
14#[cfg(test)]
15mod tests;
16
17#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(default)]
21pub struct StatisticsMaintenance {
22 object_id: Option<[u8; 16]>,
23 generation: u64,
24 changes: u64,
25 dirty_since_ms: u64,
26 analyzed_rows: Option<u64>,
27 statistics_format: u32,
28}
29
30impl StatisticsMaintenance {
31 pub const KEY_PREFIX: &'static str = "uqa.statistics.maintenance.v1:";
32
33 pub fn key(table: &str) -> String {
34 format!("{}{table}", Self::KEY_PREFIX)
35 }
36
37 pub fn load(catalog: &dyn CatalogFacade, table: &str) -> StorageBackendResult<Self> {
38 catalog
39 .get_metadata(&Self::key(table))?
40 .map(|json| serde_json::from_str(&json).map_err(Into::into))
41 .transpose()
42 .map(Option::unwrap_or_default)
43 }
44
45 pub fn load_for(
46 catalog: &dyn CatalogFacade,
47 table: &str,
48 object_id: [u8; 16],
49 ) -> StorageBackendResult<Self> {
50 let state = Self::load(catalog, table)?;
51 Ok(
52 if state.object_id.is_some_and(|stored| stored != object_id) {
53 Self::default()
54 } else {
55 state
56 },
57 )
58 }
59
60 pub fn save(&self, catalog: &dyn CatalogFacade, table: &str) -> StorageBackendResult<()> {
61 catalog.save_statistics_maintenance(table, self)
62 }
63
64 pub fn dirty(&self) -> bool {
65 self.changes != 0
66 }
67
68 pub fn missing(&self, statistics_empty: bool) -> bool {
69 self.analyzed_rows.is_none() && statistics_empty
70 }
71
72 pub fn invalidates_existing_statistics(&self) -> bool {
73 self.dirty() && self.analyzed_rows.is_some()
76 }
77
78 pub fn due(&self, missing: bool, now: u64, statistics_format: u32) -> bool {
79 self.statistics_format != statistics_format
80 || missing
81 || (self.dirty()
82 && (self.analyzed_rows == Some(0)
83 || self.changes >= 50 + self.analyzed_rows.unwrap_or(0) / 10
84 || now.saturating_sub(self.dirty_since_ms) >= MAX_DIRTY_AGE_MS))
85 }
86
87 pub fn analyzed_for(
88 catalog: &dyn CatalogFacade,
89 table: &str,
90 object_id: [u8; 16],
91 rows: u64,
92 statistics_format: u32,
93 ) -> StorageBackendResult<()> {
94 let mut state = Self::load_for(catalog, table, object_id)?;
95 state.object_id = Some(object_id);
96 state.advance_generation()?;
97 state.changes = 0;
98 state.dirty_since_ms = 0;
99 state.analyzed_rows = Some(rows);
100 state.statistics_format = statistics_format;
101 state.save(catalog, table)
102 }
103
104 fn advance_generation(&mut self) -> StorageBackendResult<()> {
105 self.generation = self.generation.checked_add(1).ok_or_else(|| {
106 crate::StorageBackendError::Other("statistics generation space exhausted".into())
107 })?;
108 Ok(())
109 }
110
111 pub fn record_changes(
112 &mut self,
113 object_id: [u8; 16],
114 count: u64,
115 previous_rows: Option<u64>,
116 now: u64,
117 ) -> StorageBackendResult<()> {
118 self.object_id = Some(object_id);
119 self.advance_generation()?;
120 if self.analyzed_rows.is_none() {
121 self.analyzed_rows = previous_rows;
122 }
123 if !self.dirty() {
124 self.dirty_since_ms = now;
125 }
126 self.changes = self.changes.checked_add(count).ok_or_else(|| {
127 crate::StorageBackendError::Other("statistics change count overflow".into())
128 })?;
129 Ok(())
130 }
131
132 pub fn encode(
134 &self,
135 control: &crate::read_control::StorageReadControl,
136 ) -> crate::mvcc::VersionResult<uqa_core::memory::BudgetedVec<u8>> {
137 crate::key_value::record_json::encode(self, control)
138 }
139
140 pub(crate) fn merge(
142 before: &Self,
143 after: &Self,
144 current: &Self,
145 ) -> crate::mvcc::VersionResult<Option<Self>> {
146 use crate::mvcc::VersionError;
147 if before
148 .object_id
149 .is_some_and(|id| Some(id) != after.object_id)
150 || current
151 .object_id
152 .is_some_and(|id| Some(id) != after.object_id)
153 || after.generation < before.generation
154 {
155 return Ok(None);
156 }
157 let changes =
158 i128::from(current.changes) + i128::from(after.changes) - i128::from(before.changes);
159 if changes < 0 {
160 return Ok(None);
161 }
162 let changes = u64::try_from(changes)
163 .map_err(|_| VersionError::InvalidEncoding("statistics change count overflow"))?;
164 let generation = current
165 .generation
166 .checked_add(after.generation - before.generation)
167 .ok_or(VersionError::InvalidEncoding(
168 "statistics generation space exhausted",
169 ))?;
170 let changed_estimate = after.statistics_format != 0
171 && (after.analyzed_rows != before.analyzed_rows
172 || after.statistics_format != before.statistics_format);
173 Ok(Some(Self {
174 object_id: after.object_id,
175 generation,
176 changes,
177 dirty_since_ms: if changes == 0 {
178 0
179 } else {
180 [after.dirty_since_ms, current.dirty_since_ms]
181 .into_iter()
182 .filter(|time| *time != 0)
183 .min()
184 .unwrap_or(0)
185 },
186 analyzed_rows: if changed_estimate {
187 after.analyzed_rows
188 } else {
189 current.analyzed_rows.or(after.analyzed_rows)
190 },
191 statistics_format: if changed_estimate {
192 after.statistics_format
193 } else {
194 current.statistics_format
195 },
196 }))
197 }
198}