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