Skip to main content

safe_migrate/ast/
identifiers.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4pub struct Ident {
5    pub text: String,
6    pub quoted: bool,
7}
8
9impl Ident {
10    pub fn new(text: impl Into<String>, quoted: bool) -> Self {
11        Self {
12            text: text.into(),
13            quoted,
14        }
15    }
16
17    /// Returns the lookup spelling used by PostgreSQL and the analyzer. Quoted
18    /// identifiers preserve case, unquoted identifiers are folded, and both are
19    /// clipped to PostgreSQL's default `NAMEDATALEN - 1` byte limit without
20    /// splitting a UTF-8 code point.
21    pub fn resolve(&self) -> String {
22        let resolved = if self.quoted {
23            self.text.clone()
24        } else {
25            self.text.to_ascii_lowercase()
26        };
27        truncate_postgres_identifier(&resolved).to_string()
28    }
29}
30
31fn truncate_postgres_identifier(value: &str) -> &str {
32    const MAX_IDENTIFIER_BYTES: usize = 63;
33
34    let mut end = value.len().min(MAX_IDENTIFIER_BYTES);
35    while !value.is_char_boundary(end) {
36        end -= 1;
37    }
38    &value[..end]
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub struct QualifiedName {
43    pub schema: Option<Ident>,
44    pub name: Ident,
45}
46
47impl QualifiedName {
48    pub fn new(schema: Option<Ident>, name: Ident) -> Self {
49        Self { schema, name }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::Ident;
56
57    #[test]
58    fn identifiers_follow_postgresql_byte_truncation() {
59        let ascii = "A".repeat(70);
60        assert_eq!(Ident::new(ascii, false).resolve(), "a".repeat(63));
61
62        let quoted = format!("{}suffix", "é".repeat(32));
63        let resolved = Ident::new(quoted, true).resolve();
64        assert_eq!(resolved.len(), 62);
65        assert_eq!(resolved, "é".repeat(31));
66    }
67}
68
69/// ObjectId represents a fully resolved, state-machine tracked database object.
70/// Its schema and name must already use their resolved lookup spelling.
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct ObjectId {
73    pub schema: String,
74    pub name: String,
75    #[serde(default)]
76    pub inferred_schema: bool,
77}
78
79impl PartialEq for ObjectId {
80    fn eq(&self, other: &Self) -> bool {
81        self.schema == other.schema && self.name == other.name
82    }
83}
84
85impl Eq for ObjectId {}
86
87impl std::hash::Hash for ObjectId {
88    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
89        self.schema.hash(state);
90        self.name.hash(state);
91    }
92}
93
94impl ObjectId {
95    pub fn new(schema: impl Into<String>, name: impl Into<String>) -> Self {
96        Self {
97            schema: schema.into(),
98            name: name.into(),
99            inferred_schema: false,
100        }
101    }
102}
103
104impl std::fmt::Display for ObjectId {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        if self.inferred_schema {
107            write!(f, "{}.{} (inferred)", self.schema, self.name)
108        } else {
109            write!(f, "{}.{}", self.schema, self.name)
110        }
111    }
112}