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;
14
15/// Declarative model of a Postgres `CREATE STATISTICS` object.
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct Statistic {
18    /// Schema-qualified statistic name (explicit names required).
19    pub qname: QualifiedName,
20    /// The target table whose columns are correlated.
21    pub target: QualifiedName,
22    /// Which kinds are enabled. At least one must be true (canon enforces).
23    pub kinds: StatisticKinds,
24    /// Column / expression list. Sorted by canon; deduped.
25    pub columns: Vec<StatisticColumn>,
26    /// `ALTER STATISTICS s SET STATISTICS n` — analyze target.
27    /// `None` = unmanaged / use PG default (-1).
28    pub statistics_target: Option<i32>,
29    /// Object owner. `None` = unmanaged (v0.3.1 lenient pattern).
30    pub owner: Option<Identifier>,
31    /// Optional `COMMENT ON STATISTICS`.
32    pub comment: Option<String>,
33}
34
35/// Which `kinds` flags are enabled on a `CREATE STATISTICS` object.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
37pub struct StatisticKinds {
38    /// `ndistinct` — multi-column n-distinct counts.
39    pub ndistinct: bool,
40    /// `dependencies` — functional dependencies between columns.
41    pub dependencies: bool,
42    /// `mcv` — most-common-value lists per column combination.
43    pub mcv: bool,
44}
45
46impl StatisticKinds {
47    /// PG's default when no kinds clause is given: all three enabled.
48    #[must_use]
49    pub const fn pg_default() -> Self {
50        Self {
51            ndistinct: true,
52            dependencies: true,
53            mcv: true,
54        }
55    }
56
57    /// True iff at least one kind is enabled. An empty bitset is illegal
58    /// at the IR level (canon rejects).
59    #[must_use]
60    pub const fn is_empty(&self) -> bool {
61        !self.ndistinct && !self.dependencies && !self.mcv
62    }
63}
64
65/// A single entry in the statistic's column list.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub enum StatisticColumn {
68    /// Plain `column_name` reference.
69    Column(Identifier),
70    /// Expression statistic (PG 14+): `(lower(name))`. Canonicalized via
71    /// `NormalizedExpr` (same canon as CHECK / USING / WITH CHECK).
72    Expression(NormalizedExpr),
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn kinds_default_is_all_true() {
81        let k = StatisticKinds::pg_default();
82        assert!(k.ndistinct && k.dependencies && k.mcv);
83        assert!(!k.is_empty());
84    }
85
86    #[test]
87    fn kinds_empty_when_all_false() {
88        let k = StatisticKinds {
89            ndistinct: false,
90            dependencies: false,
91            mcv: false,
92        };
93        assert!(k.is_empty());
94    }
95
96    #[test]
97    fn column_form_does_not_equal_expression_form() {
98        let c = StatisticColumn::Column(Identifier::from_unquoted("a").unwrap());
99        let e = StatisticColumn::Expression(NormalizedExpr::from_text("lower(a)"));
100        assert_ne!(c, e);
101    }
102}