Skip to main content

elefant_tools/models/
foreign_key.rs

1use crate::object_id::ObjectId;
2use crate::postgres_client_wrapper::FromPgChar;
3use crate::quoting::AttemptedKeywordUsage::ColumnName;
4use crate::quoting::{quote_value_string, IdentifierQuoter, Quotable, QuotableIter};
5use crate::{ElefantToolsError, PostgresSchema, PostgresTable};
6use itertools::Itertools;
7use serde::{Deserialize, Serialize};
8use std::cmp::Ordering;
9use std::str::FromStr;
10
11#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
12pub struct PostgresForeignKey {
13    pub name: String,
14    pub columns: Vec<PostgresForeignKeyColumn>,
15    pub referenced_schema: Option<String>,
16    pub referenced_table: String,
17    pub referenced_columns: Vec<PostgresForeignKeyReferencedColumn>,
18    pub update_action: ReferenceAction,
19    pub delete_action: ReferenceAction,
20    pub comment: Option<String>,
21    pub is_enforced: bool,
22    /// For temporal foreign keys, stores the constraint definition from pg_get_constraintdef().
23    pub constraint_definition: Option<String>,
24    pub object_id: ObjectId,
25}
26
27impl Default for PostgresForeignKey {
28    fn default() -> Self {
29        Self {
30            name: String::new(),
31            columns: Vec::new(),
32            referenced_schema: None,
33            referenced_table: String::new(),
34            referenced_columns: Vec::new(),
35            update_action: ReferenceAction::NoAction,
36            delete_action: ReferenceAction::NoAction,
37            comment: None,
38            is_enforced: true,
39            constraint_definition: None,
40            object_id: ObjectId::default(),
41        }
42    }
43}
44
45impl PostgresForeignKey {
46    pub fn get_create_statement(
47        &self,
48        table: &PostgresTable,
49        schema: &PostgresSchema,
50        identifier_quoter: &IdentifierQuoter,
51    ) -> String {
52        if let Some(ref constraint_def) = self.constraint_definition {
53            let mut sql = format!(
54                "alter table {}.{} add constraint {} {} not valid;",
55                schema.name.quote(identifier_quoter, ColumnName),
56                table.name.quote(identifier_quoter, ColumnName),
57                self.name.quote(identifier_quoter, ColumnName),
58                constraint_def
59            );
60
61            if let Some(comment) = &self.comment {
62                sql.push_str("\ncomment on constraint ");
63                sql.push_str(&self.name.quote(identifier_quoter, ColumnName));
64                sql.push_str(" on ");
65                sql.push_str(&schema.name.quote(identifier_quoter, ColumnName));
66                sql.push('.');
67                sql.push_str(&table.name.quote(identifier_quoter, ColumnName));
68                sql.push_str(" is ");
69                sql.push_str(&quote_value_string(comment));
70                sql.push(';');
71            }
72
73            return sql;
74        }
75
76        let mut sql = format!(
77            "alter table {}.{} add constraint {} foreign key (",
78            schema.name.quote(identifier_quoter, ColumnName),
79            table.name.quote(identifier_quoter, ColumnName),
80            self.name.quote(identifier_quoter, ColumnName)
81        );
82
83        let columns = self
84            .columns
85            .iter()
86            .sorted_by_key(|c| c.ordinal_position)
87            .map(|c| c.name.as_str())
88            .quote(identifier_quoter, ColumnName)
89            .join(", ");
90
91        sql.push_str(&columns);
92        sql.push_str(") references ");
93        let referenced_schema = self.referenced_schema.as_ref().unwrap_or(&schema.name);
94        sql.push_str(&referenced_schema.quote(identifier_quoter, ColumnName));
95        sql.push('.');
96        sql.push_str(&self.referenced_table.quote(identifier_quoter, ColumnName));
97        sql.push_str(" (");
98
99        let referenced_columns = self
100            .referenced_columns
101            .iter()
102            .sorted_by_key(|c| c.ordinal_position)
103            .map(|c| c.name.as_str())
104            .quote(identifier_quoter, ColumnName)
105            .join(", ");
106
107        sql.push_str(&referenced_columns);
108        sql.push(')');
109
110        if self.update_action != ReferenceAction::NoAction {
111            sql.push_str(" on update ");
112            sql.push_str(match self.update_action {
113                ReferenceAction::NoAction => unreachable!(),
114                ReferenceAction::Restrict => "restrict",
115                ReferenceAction::Cascade => "cascade",
116                ReferenceAction::SetNull => "set null",
117                ReferenceAction::SetDefault => "set default",
118            });
119        }
120
121        if self.delete_action != ReferenceAction::NoAction {
122            sql.push_str(" on delete ");
123            sql.push_str(match self.delete_action {
124                ReferenceAction::NoAction => unreachable!(),
125                ReferenceAction::Restrict => "restrict",
126                ReferenceAction::Cascade => "cascade",
127                ReferenceAction::SetNull => "set null",
128                ReferenceAction::SetDefault => "set default",
129            });
130        }
131
132        if self.columns.iter().any(|c| !c.affected_by_delete_action) {
133            let affected_columns = self
134                .columns
135                .iter()
136                .filter(|c| c.affected_by_delete_action)
137                .map(|c| c.name.as_str())
138                .quote(identifier_quoter, ColumnName)
139                .join(", ");
140
141            sql.push('(');
142            sql.push_str(&affected_columns);
143            sql.push(')');
144        }
145
146        if !self.is_enforced {
147            sql.push_str(" not enforced");
148        }
149
150        sql.push_str(" not valid");
151
152        sql.push(';');
153
154        if let Some(comment) = &self.comment {
155            sql.push_str("\ncomment on constraint ");
156            sql.push_str(&self.name.quote(identifier_quoter, ColumnName));
157            sql.push_str(" on ");
158            sql.push_str(&schema.name.quote(identifier_quoter, ColumnName));
159            sql.push('.');
160            sql.push_str(&table.name.quote(identifier_quoter, ColumnName));
161            sql.push_str(" is ");
162            sql.push_str(&quote_value_string(comment));
163            sql.push(';');
164        }
165
166        sql
167    }
168}
169
170impl Ord for PostgresForeignKey {
171    fn cmp(&self, other: &Self) -> Ordering {
172        self.name.cmp(&other.name)
173    }
174}
175
176impl PartialOrd for PostgresForeignKey {
177    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
178        Some(self.cmp(other))
179    }
180}
181
182#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
183pub struct PostgresForeignKeyColumn {
184    pub name: String,
185    pub ordinal_position: i32,
186    pub affected_by_delete_action: bool,
187}
188
189impl Ord for PostgresForeignKeyColumn {
190    fn cmp(&self, other: &Self) -> Ordering {
191        self.ordinal_position.cmp(&other.ordinal_position)
192    }
193}
194
195impl PartialOrd for PostgresForeignKeyColumn {
196    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
197        Some(self.cmp(other))
198    }
199}
200
201#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
202pub struct PostgresForeignKeyReferencedColumn {
203    pub name: String,
204    pub ordinal_position: i32,
205}
206
207impl Ord for PostgresForeignKeyReferencedColumn {
208    fn cmp(&self, other: &Self) -> Ordering {
209        self.ordinal_position.cmp(&other.ordinal_position)
210    }
211}
212
213impl PartialOrd for PostgresForeignKeyReferencedColumn {
214    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
215        Some(self.cmp(other))
216    }
217}
218
219#[derive(Debug, Eq, PartialEq, Copy, Clone, Serialize, Deserialize)]
220pub enum ReferenceAction {
221    NoAction,
222    Restrict,
223    Cascade,
224    SetNull,
225    SetDefault,
226}
227
228impl FromStr for ReferenceAction {
229    type Err = crate::ElefantToolsError;
230
231    fn from_str(s: &str) -> Result<Self, Self::Err> {
232        match s {
233            "a" | "NO ACTION" => Ok(ReferenceAction::NoAction),
234            "r" | "RESTRICT" => Ok(ReferenceAction::Restrict),
235            "c" | "CASCADE" => Ok(ReferenceAction::Cascade),
236            "n" | "SET NULL" => Ok(ReferenceAction::SetNull),
237            "d" | "SET DEFAULT" => Ok(ReferenceAction::SetDefault),
238            _ => Err(crate::ElefantToolsError::UnknownForeignKeyAction(
239                s.to_string(),
240            )),
241        }
242    }
243}
244
245impl FromPgChar for ReferenceAction {
246    fn from_pg_char(c: char) -> Result<Self, ElefantToolsError> {
247        match c {
248            'a' => Ok(ReferenceAction::NoAction),
249            'r' => Ok(ReferenceAction::Restrict),
250            'c' => Ok(ReferenceAction::Cascade),
251            'n' => Ok(ReferenceAction::SetNull),
252            'd' => Ok(ReferenceAction::SetDefault),
253            _ => Err(ElefantToolsError::UnknownForeignKeyAction(c.to_string())),
254        }
255    }
256}