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}