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};
10
11/// A user-defined collation managed by pgevolve.
12#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
13pub struct Collation {
14    /// Schema-qualified collation name.
15    pub qname: QualifiedName,
16    /// libc / icu / PG 17+ builtin.
17    pub provider: CollationProvider,
18    /// `lc_collate` from `pg_collation.collcollate`.
19    pub lc_collate: String,
20    /// `lc_ctype` from `pg_collation.collctype`.
21    pub lc_ctype: String,
22    /// `deterministic` toggle — default `true`. PG 12+, ICU only when false.
23    pub deterministic: bool,
24    /// Read-only `pg_collation.collversion`. Source declares as `None`;
25    /// the differ ignores this field. REFRESH VERSION management deferred
26    /// to v0.3.9.
27    pub version: Option<String>,
28    /// Lenient owner field (per v0.3.1 cross-cutting state pattern).
29    pub owner: Option<Identifier>,
30    /// `COMMENT ON COLLATION qname IS '...'`.
31    pub comment: Option<String>,
32}
33
34/// Locale-data provider — controls which OS / library produces the
35/// sort + ctype tables.
36#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum CollationProvider {
39    /// `pg_collation.collprovider = 'c'`.
40    Libc,
41    /// `pg_collation.collprovider = 'i'`.
42    Icu,
43    /// `pg_collation.collprovider = 'b'` — PG 17+ only.
44    Builtin,
45}
46
47/// Collation shortnames that bypass `column-references-unmanaged-collation`
48/// even when they have no schema qualifier. PG seeds these at initdb.
49pub const BUILTIN_COLLATIONS: &[&str] =
50    &["default", "C", "POSIX", "und-x-icu", "unicode", "ucs_basic"];
51
52impl CollationProvider {
53    /// SQL keyword used in `CREATE COLLATION … (provider = …)`.
54    #[must_use]
55    pub const fn sql_keyword(self) -> &'static str {
56        match self {
57            Self::Libc => "libc",
58            Self::Icu => "icu",
59            Self::Builtin => "builtin",
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn id(s: &str) -> Identifier {
69        Identifier::from_unquoted(s).unwrap()
70    }
71    fn qn(s: &str, n: &str) -> QualifiedName {
72        QualifiedName::new(id(s), id(n))
73    }
74
75    #[test]
76    fn collation_serde_round_trip() {
77        let c = Collation {
78            qname: qn("app", "case_insensitive"),
79            provider: CollationProvider::Icu,
80            lc_collate: "und".into(),
81            lc_ctype: "und".into(),
82            deterministic: false,
83            version: None,
84            owner: None,
85            comment: Some("CI collation".into()),
86        };
87        let json = serde_json::to_string(&c).unwrap();
88        let back: Collation = serde_json::from_str(&json).unwrap();
89        assert_eq!(c, back);
90    }
91
92    #[test]
93    fn provider_sql_keywords() {
94        assert_eq!(CollationProvider::Libc.sql_keyword(), "libc");
95        assert_eq!(CollationProvider::Icu.sql_keyword(), "icu");
96        assert_eq!(CollationProvider::Builtin.sql_keyword(), "builtin");
97    }
98}