Skip to main content

pgevolve_core/ir/
statistic.rs

1//! Statistic IR — declarative model for Postgres CREATE STATISTICS.
2//!
3//! pgevolve manages `pg_statistic_ext` objects with explicit names. Source
4//! must declare the name (`CREATE STATISTICS app.s ON (...) FROM app.t`);
5//! anonymous form `CREATE STATISTICS ON (...) FROM app.t` is rejected at
6//! parse time, mirroring the no-anonymous-indexes policy.
7//!
8//! Spec: `docs/superpowers/specs/2026-05-27-statistics-and-check-option-design.md`.
9
10use serde::{Deserialize, Serialize};
11
12use crate::identifier::{Identifier, QualifiedName};
13use crate::ir::default_expr::NormalizedExpr;
14use crate::ir::difference::Difference;
15use crate::ir::eq::{Equiv, field_difference};
16
17/// Declarative model of a Postgres `CREATE STATISTICS` object.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub struct Statistic {
20    /// Schema-qualified statistic name (explicit names required).
21    pub qname: QualifiedName,
22    /// The target table whose columns are correlated.
23    pub target: QualifiedName,
24    /// Which kinds are enabled. At least one must be true (canon enforces).
25    pub kinds: StatisticKinds,
26    /// Column / expression list. Sorted by canon; deduped.
27    pub columns: Vec<StatisticColumn>,
28    /// `ALTER STATISTICS s SET STATISTICS n` — analyze target.
29    /// `None` = unmanaged / use PG default (-1).
30    pub statistics_target: Option<i32>,
31    /// Object owner. `None` = unmanaged (v0.3.1 lenient pattern).
32    pub owner: Option<Identifier>,
33    /// Optional `COMMENT ON STATISTICS`.
34    pub comment: Option<String>,
35}
36
37impl Equiv for Statistic {
38    fn differences(&self, other: &Self) -> Vec<Difference> {
39        // Field-completeness guard: the compiler errors if a field is added
40        // without being handled below. Bindings are unused (read via `self`).
41        let Self {
42            qname: _,
43            target: _,
44            kinds: _,
45            columns: _,
46            statistics_target: _,
47            owner: _,
48            comment: _,
49        } = self;
50        let mut out = Vec::new();
51        out.extend(field_difference("qname", &self.qname, &other.qname));
52        out.extend(field_difference("target", &self.target, &other.target));
53        out.extend(field_difference(
54            "kinds",
55            &format!("{:?}", self.kinds),
56            &format!("{:?}", other.kinds),
57        ));
58        out.extend(field_difference(
59            "columns",
60            &format!("{:?}", self.columns),
61            &format!("{:?}", other.columns),
62        ));
63        out.extend(field_difference(
64            "statistics_target",
65            &format!("{:?}", self.statistics_target),
66            &format!("{:?}", other.statistics_target),
67        ));
68        out.extend(field_difference(
69            "owner",
70            &format!("{:?}", self.owner),
71            &format!("{:?}", other.owner),
72        ));
73        out.extend(field_difference(
74            "comment",
75            &format!("{:?}", self.comment),
76            &format!("{:?}", other.comment),
77        ));
78        out
79    }
80}
81
82/// Which `kinds` flags are enabled on a `CREATE STATISTICS` object.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
84pub struct StatisticKinds {
85    /// `ndistinct` — multi-column n-distinct counts.
86    pub ndistinct: bool,
87    /// `dependencies` — functional dependencies between columns.
88    pub dependencies: bool,
89    /// `mcv` — most-common-value lists per column combination.
90    pub mcv: bool,
91}
92
93impl StatisticKinds {
94    /// PG's default when no kinds clause is given: all three enabled.
95    #[must_use]
96    pub const fn pg_default() -> Self {
97        Self {
98            ndistinct: true,
99            dependencies: true,
100            mcv: true,
101        }
102    }
103
104    /// True iff at least one kind is enabled. An empty bitset is illegal
105    /// at the IR level (canon rejects).
106    #[must_use]
107    pub const fn is_empty(&self) -> bool {
108        !self.ndistinct && !self.dependencies && !self.mcv
109    }
110}
111
112/// A single entry in the statistic's column list.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub enum StatisticColumn {
115    /// Plain `column_name` reference.
116    Column(Identifier),
117    /// Expression statistic (PG 14+): `(lower(name))`. Canonicalized via
118    /// `NormalizedExpr` (same canon as CHECK / USING / WITH CHECK).
119    Expression(NormalizedExpr),
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125
126    #[test]
127    fn kinds_default_is_all_true() {
128        let k = StatisticKinds::pg_default();
129        assert!(k.ndistinct && k.dependencies && k.mcv);
130        assert!(!k.is_empty());
131    }
132
133    #[test]
134    fn kinds_empty_when_all_false() {
135        let k = StatisticKinds {
136            ndistinct: false,
137            dependencies: false,
138            mcv: false,
139        };
140        assert!(k.is_empty());
141    }
142
143    #[test]
144    fn column_form_does_not_equal_expression_form() {
145        let c = StatisticColumn::Column(Identifier::from_unquoted("a").unwrap());
146        let e = StatisticColumn::Expression(NormalizedExpr::from_text("lower(a)"));
147        assert_ne!(c, e);
148    }
149}