Skip to main content

pgevolve_core/ir/
cast.rs

1//! `CAST` IR — a global (non-schema-scoped) object. Identity is `(source, target)`.
2
3use serde::{Deserialize, Serialize};
4
5use crate::identifier::QualifiedName;
6use crate::ir::column_type::ColumnType;
7use crate::ir::difference::Difference;
8use crate::ir::eq::{Equiv, field_difference};
9
10/// A `CREATE CAST` object. Identity is `(source, target)`.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct Cast {
13    /// Source type (part of identity).
14    pub source: QualifiedName,
15    /// Target type (part of identity).
16    pub target: QualifiedName,
17    /// How the cast is performed.
18    pub method: CastMethod,
19    /// When the cast is applied implicitly.
20    pub context: CastContext,
21    /// Optional comment.
22    pub comment: Option<String>,
23}
24
25impl Equiv for Cast {
26    fn differences(&self, other: &Self) -> Vec<Difference> {
27        // Field-completeness guard: the compiler errors if a field is added
28        // without being handled below. Bindings are unused (read via `self`).
29        let Self {
30            source: _,
31            target: _,
32            method: _,
33            context: _,
34            comment: _,
35        } = self;
36        let mut out = Vec::new();
37        out.extend(field_difference("source", &self.source, &other.source));
38        out.extend(field_difference("target", &self.target, &other.target));
39        out.extend(field_difference(
40            "method",
41            &format!("{:?}", self.method),
42            &format!("{:?}", other.method),
43        ));
44        out.extend(field_difference(
45            "context",
46            &format!("{:?}", self.context),
47            &format!("{:?}", other.context),
48        ));
49        out.extend(field_difference(
50            "comment",
51            &format!("{:?}", self.comment),
52            &format!("{:?}", other.comment),
53        ));
54        out
55    }
56}
57
58/// The conversion mechanism for a `CREATE CAST`.
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum CastMethod {
61    /// `WITH FUNCTION fn(argtypes)` — a managed function.
62    ///
63    /// `arg_types` is the recorded conversion-function signature
64    /// (1–3 args: source\[, int4, bool\]).
65    Function {
66        /// Schema-qualified function name.
67        name: QualifiedName,
68        /// Argument types of the conversion function.
69        arg_types: Vec<ColumnType>,
70    },
71    /// `WITH INOUT` — uses the types' I/O functions.
72    Inout,
73    /// `WITHOUT FUNCTION` — binary-coercible; no function required.
74    Binary,
75}
76
77/// Controls when Postgres automatically applies the cast.
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum CastContext {
81    /// Default — explicit casts only (`CAST(x AS t)` or `x::t`).
82    Explicit,
83    /// `AS ASSIGNMENT` — implicitly applied on assignment.
84    Assignment,
85    /// `AS IMPLICIT` — applied implicitly in expressions.
86    Implicit,
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92    use crate::identifier::{Identifier, QualifiedName};
93
94    fn id(s: &str) -> Identifier {
95        Identifier::from_unquoted(s).unwrap()
96    }
97
98    fn qname(schema: &str, name: &str) -> QualifiedName {
99        QualifiedName::new(id(schema), id(name))
100    }
101
102    fn cast_function() -> Cast {
103        Cast {
104            source: qname("app", "my_type"),
105            target: qname("pg_catalog", "text"),
106            method: CastMethod::Function {
107                name: qname("app", "my_type_to_text"),
108                arg_types: vec![ColumnType::Integer],
109            },
110            context: CastContext::Explicit,
111            comment: Some("Converts my_type to text.".to_string()),
112        }
113    }
114
115    fn cast_inout() -> Cast {
116        Cast {
117            source: qname("app", "domain_a"),
118            target: qname("app", "domain_b"),
119            method: CastMethod::Inout,
120            context: CastContext::Assignment,
121            comment: None,
122        }
123    }
124
125    fn cast_binary() -> Cast {
126        Cast {
127            source: qname("app", "type_x"),
128            target: qname("app", "type_y"),
129            method: CastMethod::Binary,
130            context: CastContext::Implicit,
131            comment: None,
132        }
133    }
134
135    #[test]
136    fn cast_function_serde_round_trip() {
137        let c = cast_function();
138        let json = serde_json::to_string(&c).unwrap();
139        let back: Cast = serde_json::from_str(&json).unwrap();
140        assert_eq!(c, back);
141    }
142
143    #[test]
144    fn cast_inout_serde_round_trip() {
145        let c = cast_inout();
146        let json = serde_json::to_string(&c).unwrap();
147        let back: Cast = serde_json::from_str(&json).unwrap();
148        assert_eq!(c, back);
149    }
150
151    #[test]
152    fn cast_binary_serde_round_trip() {
153        let c = cast_binary();
154        let json = serde_json::to_string(&c).unwrap();
155        let back: Cast = serde_json::from_str(&json).unwrap();
156        assert_eq!(c, back);
157    }
158
159    #[test]
160    fn cast_context_serde_variants() {
161        let explicit = serde_json::to_string(&CastContext::Explicit).unwrap();
162        let assignment = serde_json::to_string(&CastContext::Assignment).unwrap();
163        let implicit = serde_json::to_string(&CastContext::Implicit).unwrap();
164        assert_eq!(explicit, "\"explicit\"");
165        assert_eq!(assignment, "\"assignment\"");
166        assert_eq!(implicit, "\"implicit\"");
167
168        let back_e: CastContext = serde_json::from_str(&explicit).unwrap();
169        let back_a: CastContext = serde_json::from_str(&assignment).unwrap();
170        let back_i: CastContext = serde_json::from_str(&implicit).unwrap();
171        assert_eq!(back_e, CastContext::Explicit);
172        assert_eq!(back_a, CastContext::Assignment);
173        assert_eq!(back_i, CastContext::Implicit);
174    }
175}