Skip to main content

uqa_storage/sqlite/catalog/
stats.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Persisted column statistics.
8
9use super::{params, Catalog, ColumnStatsInput, ColumnStatsRow, Result, SQLiteError};
10
11impl Catalog {
12    /// Persist a per-column ANALYZE summary so the planner still has
13    /// cardinality / range estimates after a restart. `min_value` and
14    /// `max_value` are stored as strings (JSON when the value isn't
15    /// natively textual) so the column type is irrelevant.
16    pub fn save_column_stats(&self, stats: ColumnStatsInput<'_>) -> Result<()> {
17        self.conn.with(|c| {
18            c.execute(
19                "INSERT OR REPLACE INTO _column_stats
20                    (table_name, column_name, distinct_count, null_count,
21                     min_value, max_value, row_count,
22                     histogram, mcv_values, mcv_frequencies)
23                 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
24                params![
25                    stats.table_name,
26                    stats.column_name,
27                    stats.distinct_count,
28                    stats.null_count,
29                    stats.min_value,
30                    stats.max_value,
31                    stats.row_count,
32                    stats.histogram_json,
33                    stats.mcv_values_json,
34                    stats.mcv_frequencies_json,
35                ],
36            )?;
37            Ok(())
38        })
39    }
40
41    pub fn replace_column_stats(
42        &self,
43        table_name: &str,
44        stats: &[ColumnStatsInput<'_>],
45    ) -> Result<()> {
46        if let Some(row) = stats.iter().find(|row| row.table_name != table_name) {
47            return Err(SQLiteError::StorageBackend(format!(
48                "column stats row for table `{}` cannot be stored in snapshot `{table_name}`",
49                row.table_name
50            )));
51        }
52        self.conn.with_mut(|connection| {
53            let transaction = connection.savepoint()?;
54            transaction.execute(
55                "DELETE FROM _column_stats WHERE table_name = ?1",
56                params![table_name],
57            )?;
58            {
59                let mut statement = transaction.prepare(
60                    "INSERT INTO _column_stats
61                        (table_name, column_name, distinct_count, null_count,
62                         min_value, max_value, row_count,
63                         histogram, mcv_values, mcv_frequencies)
64                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)",
65                )?;
66                for row in stats {
67                    statement.execute(params![
68                        row.table_name,
69                        row.column_name,
70                        row.distinct_count,
71                        row.null_count,
72                        row.min_value,
73                        row.max_value,
74                        row.row_count,
75                        row.histogram_json,
76                        row.mcv_values_json,
77                        row.mcv_frequencies_json,
78                    ])?;
79                }
80            }
81            transaction.commit()?;
82            Ok(())
83        })
84    }
85
86    pub fn load_column_stats(&self, table_name: &str) -> Result<Vec<ColumnStatsRow>> {
87        self.conn.with(|c| {
88            let mut stmt = c.prepare(
89                "SELECT column_name, distinct_count, null_count,
90                        min_value, max_value, row_count,
91                        histogram, mcv_values, mcv_frequencies
92                   FROM _column_stats
93                  WHERE table_name = ?1
94                  ORDER BY column_name",
95            )?;
96            let rows = stmt.query_map(params![table_name], |r| {
97                Ok(ColumnStatsRow {
98                    column_name: r.get::<_, String>(0)?,
99                    distinct_count: r.get::<_, i64>(1)?,
100                    null_count: r.get::<_, i64>(2)?,
101                    min_value: r.get::<_, Option<String>>(3)?,
102                    max_value: r.get::<_, Option<String>>(4)?,
103                    row_count: r.get::<_, i64>(5)?,
104                    histogram_json: r.get::<_, String>(6)?,
105                    mcv_values_json: r.get::<_, String>(7)?,
106                    mcv_frequencies_json: r.get::<_, String>(8)?,
107                })
108            })?;
109            let mut out = Vec::new();
110            for row in rows {
111                out.push(row?);
112            }
113            Ok(out)
114        })
115    }
116
117    pub fn delete_column_stats(&self, table_name: &str) -> Result<()> {
118        self.conn.with(|c| {
119            c.execute(
120                "DELETE FROM _column_stats WHERE table_name = ?1",
121                params![table_name],
122            )?;
123            Ok(())
124        })
125    }
126}