Skip to main content

pgevolve_core/ir/
collation.rs

1//! `CREATE COLLATION` IR — first-class managed collation kind.
2//!
3//! Source `lc_collate` and `lc_ctype` are always stored separately, even
4//! when the user wrote `locale = 'X'` shorthand. The renderer collapses
5//! back to `locale = '...'` when the two are equal.
6
7use serde::{Deserialize, Serialize};
8
9use crate::identifier::{Identifier, QualifiedName};
10use crate::ir::difference::Difference;
11use crate::ir::eq::{Equiv, field_difference};
12
13/// A user-defined collation managed by pgevolve.
14#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
15pub struct Collation {
16    /// Schema-qualified collation name.
17    pub qname: QualifiedName,
18    /// libc / icu / PG 17+ builtin.
19    pub provider: CollationProvider,
20    /// `lc_collate` from `pg_collation.collcollate`.
21    pub lc_collate: String,
22    /// `lc_ctype` from `pg_collation.collctype`.
23    pub lc_ctype: String,
24    /// `deterministic` toggle — default `true`. PG 12+, ICU only when false.
25    pub deterministic: bool,
26    /// Read-only `pg_collation.collversion`. Source declares as `None`;
27    /// the differ ignores this field. REFRESH VERSION management deferred
28    /// to v0.3.9.
29    pub version: Option<String>,
30    /// Lenient owner field (per v0.3.1 cross-cutting state pattern).
31    pub owner: Option<Identifier>,
32    /// `COMMENT ON COLLATION qname IS '...'`.
33    pub comment: Option<String>,
34}
35
36impl Equiv for Collation {
37    fn differences(&self, other: &Self) -> Vec<Difference> {
38        // Field-completeness guard: the compiler errors if a field is added
39        // without being handled below. `version` is read-only (`pg_collation.
40        // collversion`); source declares it `None` and the differ ignores it,
41        // so it is intentionally excluded from equivalence.
42        let Self {
43            qname: _,
44            provider: _,
45            lc_collate: _,
46            lc_ctype: _,
47            deterministic: _,
48            version: _, // read-only collversion, ignored by the differ
49            owner: _,
50            comment: _,
51        } = self;
52        let mut out = Vec::new();
53        out.extend(field_difference("qname", &self.qname, &other.qname));
54        out.extend(field_difference(
55            "provider",
56            &format!("{:?}", self.provider),
57            &format!("{:?}", other.provider),
58        ));
59        out.extend(field_difference(
60            "lc_collate",
61            &self.lc_collate,
62            &other.lc_collate,
63        ));
64        out.extend(field_difference(
65            "lc_ctype",
66            &self.lc_ctype,
67            &other.lc_ctype,
68        ));
69        out.extend(field_difference(
70            "deterministic",
71            &format!("{:?}", self.deterministic),
72            &format!("{:?}", other.deterministic),
73        ));
74        out.extend(field_difference(
75            "owner",
76            &format!("{:?}", self.owner),
77            &format!("{:?}", other.owner),
78        ));
79        out.extend(field_difference(
80            "comment",
81            &format!("{:?}", self.comment),
82            &format!("{:?}", other.comment),
83        ));
84        out
85    }
86}
87
88/// Locale-data provider — controls which OS / library produces the
89/// sort + ctype tables.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum CollationProvider {
93    /// `pg_collation.collprovider = 'c'`.
94    Libc,
95    /// `pg_collation.collprovider = 'i'`.
96    Icu,
97    /// `pg_collation.collprovider = 'b'` — PG 17+ only.
98    Builtin,
99}
100
101/// Collation shortnames that bypass `column-references-unmanaged-collation`
102/// even when they have no schema qualifier. PG seeds these at initdb.
103pub const BUILTIN_COLLATIONS: &[&str] =
104    &["default", "C", "POSIX", "und-x-icu", "unicode", "ucs_basic"];
105
106impl CollationProvider {
107    /// SQL keyword used in `CREATE COLLATION … (provider = …)`.
108    #[must_use]
109    pub const fn sql_keyword(self) -> &'static str {
110        match self {
111            Self::Libc => "libc",
112            Self::Icu => "icu",
113            Self::Builtin => "builtin",
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    fn id(s: &str) -> Identifier {
123        Identifier::from_unquoted(s).unwrap()
124    }
125    fn qn(s: &str, n: &str) -> QualifiedName {
126        QualifiedName::new(id(s), id(n))
127    }
128
129    #[test]
130    fn collation_serde_round_trip() {
131        let c = Collation {
132            qname: qn("app", "case_insensitive"),
133            provider: CollationProvider::Icu,
134            lc_collate: "und".into(),
135            lc_ctype: "und".into(),
136            deterministic: false,
137            version: None,
138            owner: None,
139            comment: Some("CI collation".into()),
140        };
141        let json = serde_json::to_string(&c).unwrap();
142        let back: Collation = serde_json::from_str(&json).unwrap();
143        assert_eq!(c, back);
144    }
145
146    #[test]
147    fn provider_sql_keywords() {
148        assert_eq!(CollationProvider::Libc.sql_keyword(), "libc");
149        assert_eq!(CollationProvider::Icu.sql_keyword(), "icu");
150        assert_eq!(CollationProvider::Builtin.sql_keyword(), "builtin");
151    }
152}