Skip to main content

safe_migrate/model/
relation.rs

1// FILE: src/model/relation.rs
2use crate::ast::identifiers::ObjectId;
3use crate::model::column::Column;
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum Privilege {
9    Select,
10    Insert,
11    Update,
12    Delete,
13    Truncate,
14    References,
15    Trigger,
16    All,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
20pub struct PrivilegeMatrix {
21    /// Maps role identity to the set of privileges they possess on this relation
22    pub grants: HashMap<ObjectId, HashSet<Privilege>>,
23}
24
25impl PrivilegeMatrix {
26    pub fn grant(&mut self, role: ObjectId, privileges: HashSet<Privilege>) {
27        self.grants.entry(role).or_default().extend(privileges);
28    }
29
30    pub fn revoke(&mut self, role: &ObjectId, privileges: &HashSet<Privilege>) {
31        if let Some(owned) = self.grants.get_mut(role) {
32            if privileges.contains(&Privilege::All) {
33                owned.clear();
34            } else {
35                for p in privileges {
36                    owned.remove(p);
37                }
38            }
39        }
40    }
41
42    pub fn has_privilege(&self, role: &ObjectId, privilege: Privilege) -> bool {
43        self.grants.get(role).is_some_and(|set| {
44            set.contains(&privilege)
45                || (privilege != Privilege::All && set.contains(&Privilege::All))
46        })
47    }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
51pub enum RelationKind {
52    Table,
53    View,
54    MaterializedView,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58pub enum Persistence {
59    Permanent,
60    Temporary,
61    Unlogged,
62}
63
64#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub struct RelationState {
66    pub id: ObjectId,
67    pub owner: ObjectId,
68    pub columns: Vec<Column>,
69    pub generation: u64,
70    pub estimated_rows: Option<u64>,
71    pub relpages: Option<u64>,
72    pub kind: RelationKind,
73    pub persistence: Persistence,
74    pub triggers: HashSet<String>,
75    pub policies: HashSet<String>,
76    pub last_analyze: Option<String>,
77    pub last_autoanalyze: Option<String>,
78    pub created_at_tx_depth: usize, // Phase 1 FIX: Same-Transaction index tracking
79    pub privileges: PrivilegeMatrix,
80    pub partition_type: Option<String>, // e.g., "RANGE", "LIST", "HASH"
81    pub partition_by: Option<String>,   // The partition key expression
82    #[serde(default)]
83    pub is_fk_dependency: bool,
84}
85
86impl Default for RelationState {
87    fn default() -> Self {
88        Self {
89            id: ObjectId::new("public", "dummy"),
90            owner: ObjectId::new("public", "postgres"),
91            columns: Vec::new(),
92            generation: 0,
93            estimated_rows: Some(0),
94            relpages: None,
95            kind: RelationKind::Table,
96            persistence: Persistence::Permanent,
97            triggers: HashSet::new(),
98            policies: HashSet::new(),
99            last_analyze: None,
100            last_autoanalyze: None,
101            created_at_tx_depth: 0,
102            privileges: PrivilegeMatrix::default(),
103            partition_type: None,
104            partition_by: None,
105            is_fk_dependency: false,
106        }
107    }
108}
109
110impl RelationState {
111    pub fn new(
112        id: ObjectId,
113        owner: ObjectId,
114        generation: u64,
115        estimated_rows: Option<u64>,
116        kind: RelationKind,
117        persistence: Persistence,
118        created_at_tx_depth: usize,
119    ) -> Self {
120        Self {
121            id,
122            owner,
123            columns: Vec::new(),
124            generation,
125            estimated_rows,
126            relpages: None,
127            kind,
128            persistence,
129            triggers: HashSet::new(),
130            policies: HashSet::new(),
131            last_analyze: None,
132            last_autoanalyze: None,
133            created_at_tx_depth,
134            privileges: PrivilegeMatrix::default(),
135            partition_type: None,
136            partition_by: None,
137            is_fk_dependency: false,
138        }
139    }
140
141    pub fn mark_fk_dependency(&mut self) {
142        self.is_fk_dependency = true;
143    }
144
145    pub fn apply_column_action(&mut self, action: &ColumnAction) {
146        match action {
147            ColumnAction::Add {
148                name,
149                data_type,
150                not_null,
151                default,
152            } => {
153                if !self.columns.iter().any(|c| c.name == *name) {
154                    self.columns.push(Column {
155                        name: name.clone(),
156                        data_type: data_type.clone(),
157                        default: default.clone(),
158                        is_nullable: !not_null,
159                        avg_width: None,
160                        default_expr_text: None,
161                        type_modifier: None,
162                    });
163                }
164            }
165            ColumnAction::Drop { name } => {
166                self.columns.retain(|c| c.name != *name);
167            }
168            ColumnAction::Rename { from, to } => {
169                if let Some(pos) = self.columns.iter().position(|c| c.name == *from)
170                    && !self.columns.iter().any(|c| c.name == *to)
171                {
172                    self.columns[pos].name = to.clone();
173                }
174            }
175            ColumnAction::SetNotNull { name } => {
176                if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
177                    col.is_nullable = false;
178                }
179            }
180            ColumnAction::DropNotNull { name } => {
181                if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
182                    col.is_nullable = true;
183                }
184            }
185            ColumnAction::SetType { name, data_type } => {
186                if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
187                    col.data_type = Some(data_type.clone());
188                }
189            }
190            ColumnAction::SetDefault { name, default } => {
191                if let Some(col) = self.columns.iter_mut().find(|c| c.name == *name) {
192                    col.default = default.clone();
193                }
194            }
195        }
196    }
197
198    pub fn has_column(&self, name: &str) -> bool {
199        self.columns.iter().any(|c| c.name == name)
200    }
201
202    pub fn get_column(&self, name: &str) -> Option<&Column> {
203        self.columns.iter().find(|c| c.name == name)
204    }
205
206    pub fn is_stale(&self) -> bool {
207        self.last_analyze.is_none() && self.last_autoanalyze.is_none()
208    }
209}
210
211#[derive(Debug, Clone, PartialEq)]
212pub enum ColumnAction {
213    Add {
214        name: String,
215        data_type: Option<String>,
216        not_null: bool,
217        default: Option<crate::analysis::expr_ir::ExprIr>,
218    },
219    Drop {
220        name: String,
221    },
222    Rename {
223        from: String,
224        to: String,
225    },
226    SetNotNull {
227        name: String,
228    },
229    DropNotNull {
230        name: String,
231    },
232    SetType {
233        name: String,
234        data_type: String,
235    },
236    SetDefault {
237        name: String,
238        default: Option<crate::analysis::expr_ir::ExprIr>,
239    },
240}
241
242#[allow(clippy::large_enum_variant)]
243#[derive(Debug, Clone, PartialEq)]
244pub enum RelationOverlay {
245    Present(RelationState),
246    Dropped,
247}