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