Skip to main content

remodel_core/models/
logical.rs

1//! Logical (relational) model.
2//!
3//! Tables, columns, and constraints. Constraints carry their own definition
4//! (PK / UNIQUE column lists, FK source-and-target references) which the SQL
5//! exporter renders as either inline `CREATE TABLE` clauses or out-of-line
6//! `ALTER TABLE … ADD CONSTRAINT` statements.
7//!
8//! ## Mapping from brModelo
9//!
10//! | brModelo class    | RemodelCore type     |
11//! |-------------------|----------------------|
12//! | `Tabela`          | [`Table`]            |
13//! | `Campo`           | [`Column`]           |
14//! | `Constraint`      | [`Constraint`]       |
15//! | `DataBaseModel`   | [`LogicalModel`]     |
16
17use indexmap::IndexMap;
18use serde::{Deserialize, Serialize};
19
20use crate::error::{Error, Result};
21use crate::models::types::DataType;
22
23/// Strongly-typed handle for a [`Table`].
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
25pub struct TableId(pub u32);
26
27/// Strongly-typed handle for a [`Column`].
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
29pub struct ColumnId(pub u32);
30
31/// Strongly-typed handle for a [`Constraint`].
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
33pub struct ConstraintId(pub u32);
34
35/// A column on a [`Table`].
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct Column {
38    /// Unique handle within the owning model.
39    pub id: ColumnId,
40    /// Column name.
41    pub name: String,
42    /// Logical data type.
43    pub data_type: DataType,
44    /// Whether the column admits NULL.
45    pub nullable: bool,
46    /// `true` when this column is part of the primary key.
47    pub is_primary: bool,
48    /// `true` when this column is part of (any) foreign key.
49    pub is_foreign: bool,
50    /// `true` when this column is part of a single-column UNIQUE constraint.
51    pub is_unique: bool,
52    /// Optional default value, rendered verbatim into DDL.
53    pub default: Option<String>,
54    /// Optional descriptive comment.
55    pub comment: String,
56}
57
58impl Column {
59    /// Construct a non-key, non-null column.
60    pub fn new(id: ColumnId, name: impl Into<String>, data_type: DataType) -> Self {
61        Self {
62            id,
63            name: name.into(),
64            data_type,
65            nullable: false,
66            is_primary: false,
67            is_foreign: false,
68            is_unique: false,
69            default: None,
70            comment: String::new(),
71        }
72    }
73}
74
75/// What action a referential constraint takes when the parent row changes.
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
77pub enum ReferentialAction {
78    /// `NO ACTION` — the default; the operation is rejected if it would
79    /// orphan a referencing row.
80    #[default]
81    NoAction,
82    /// `RESTRICT` — like `NO ACTION` but checked immediately.
83    Restrict,
84    /// `CASCADE` — propagate the change to the referencing rows.
85    Cascade,
86    /// `SET NULL` — set the FK columns to NULL.
87    SetNull,
88    /// `SET DEFAULT` — set the FK columns to their default values.
89    SetDefault,
90}
91
92/// A foreign key payload, attached to a [`Constraint`] of kind
93/// [`ConstraintKind::ForeignKey`].
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct ForeignKey {
96    /// Local column IDs (ordered) that constitute the FK.
97    pub columns: Vec<ColumnId>,
98    /// The table that this FK references.
99    pub references_table: TableId,
100    /// Column IDs of the referenced primary/unique key, in matching order.
101    pub references_columns: Vec<ColumnId>,
102    /// Action on parent UPDATE.
103    pub on_update: ReferentialAction,
104    /// Action on parent DELETE.
105    pub on_delete: ReferentialAction,
106}
107
108/// What kind of constraint this is.
109#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
110pub enum ConstraintKind {
111    /// Primary key over the listed columns.
112    PrimaryKey {
113        /// Columns that compose the primary key, in order.
114        columns: Vec<ColumnId>,
115    },
116    /// Uniqueness constraint over the listed columns.
117    Unique {
118        /// Columns that must be jointly unique.
119        columns: Vec<ColumnId>,
120    },
121    /// Foreign key. See [`ForeignKey`].
122    ForeignKey(ForeignKey),
123    /// CHECK constraint with a verbatim predicate (rendered as-is).
124    Check {
125        /// SQL expression for the predicate.
126        expression: String,
127    },
128}
129
130/// A named (or unnamed) constraint on a table.
131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
132pub struct Constraint {
133    /// Unique handle within the owning model.
134    pub id: ConstraintId,
135    /// Optional explicit name. If `None`, the SQL exporter will emit it
136    /// inline (for `PRIMARY KEY` / `UNIQUE`) or synthesise a name (for `FK`).
137    pub name: Option<String>,
138    /// Constraint kind and payload.
139    pub kind: ConstraintKind,
140}
141
142impl Constraint {
143    /// `true` if this is a primary-key constraint.
144    pub fn is_primary_key(&self) -> bool {
145        matches!(self.kind, ConstraintKind::PrimaryKey { .. })
146    }
147
148    /// `true` if this is a foreign-key constraint.
149    pub fn is_foreign_key(&self) -> bool {
150        matches!(self.kind, ConstraintKind::ForeignKey(_))
151    }
152}
153
154/// A relational table.
155#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
156pub struct Table {
157    /// Unique handle within the owning model.
158    pub id: TableId,
159    /// Table name.
160    pub name: String,
161    /// Columns, in author order.
162    pub columns: IndexMap<ColumnId, Column>,
163    /// Constraints attached to this table (PK, UNIQUE, FK, CHECK).
164    pub constraints: IndexMap<ConstraintId, Constraint>,
165    /// Optional descriptive comment.
166    pub comment: String,
167}
168
169impl Table {
170    /// Iterate the columns in author order.
171    pub fn columns_iter(&self) -> impl Iterator<Item = &Column> {
172        self.columns.values()
173    }
174
175    /// Find the table's primary-key constraint, if any.
176    pub fn primary_key(&self) -> Option<&Constraint> {
177        self.constraints.values().find(|c| c.is_primary_key())
178    }
179
180    /// IDs of the columns that compose the primary key. Empty if there is no
181    /// PK or the PK is empty.
182    pub fn primary_key_columns(&self) -> &[ColumnId] {
183        match self.primary_key().map(|c| &c.kind) {
184            Some(ConstraintKind::PrimaryKey { columns }) => columns,
185            _ => &[],
186        }
187    }
188
189    /// Borrow a column by ID.
190    pub fn column(&self, id: ColumnId) -> Option<&Column> {
191        self.columns.get(&id)
192    }
193}
194
195/// The full logical (relational) model.
196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
197pub struct LogicalModel {
198    /// Database / schema name.
199    pub name: String,
200    /// Monotonic counter used to mint unique IDs.
201    next_id: u32,
202    /// Tables, keyed for O(1) lookup; iteration order is insertion order.
203    pub tables: IndexMap<TableId, Table>,
204}
205
206impl LogicalModel {
207    /// Construct a new empty logical model.
208    pub fn new(name: impl Into<String>) -> Self {
209        Self { name: name.into(), ..Self::default() }
210    }
211
212    pub(crate) fn mint(&mut self) -> u32 {
213        self.next_id = self.next_id.checked_add(1).expect("ID space exhausted");
214        self.next_id
215    }
216
217    /// Create a new empty table.
218    pub fn add_table(&mut self, name: impl Into<String>) -> TableId {
219        let id = TableId(self.mint());
220        self.tables.insert(
221            id,
222            Table {
223                id,
224                name: name.into(),
225                columns: IndexMap::new(),
226                constraints: IndexMap::new(),
227                comment: String::new(),
228            },
229        );
230        id
231    }
232
233    /// Borrow a table by handle.
234    pub fn table(&self, id: TableId) -> Result<&Table> {
235        self.tables
236            .get(&id)
237            .ok_or_else(|| Error::UnknownReference { kind: "table", id: format!("{}", id.0) })
238    }
239
240    /// Mutably borrow a table by handle.
241    pub fn table_mut(&mut self, id: TableId) -> Result<&mut Table> {
242        self.tables
243            .get_mut(&id)
244            .ok_or_else(|| Error::UnknownReference { kind: "table", id: format!("{}", id.0) })
245    }
246
247    /// Append a new column to `table`.
248    pub fn add_column(
249        &mut self,
250        table: TableId,
251        name: impl Into<String>,
252        data_type: DataType,
253    ) -> Result<ColumnId> {
254        let id = ColumnId(self.mint());
255        let column = Column::new(id, name, data_type);
256        self.table_mut(table)?.columns.insert(id, column);
257        Ok(id)
258    }
259
260    /// Add (or replace) a primary-key constraint on `table`.
261    pub fn set_primary_key(&mut self, table: TableId, columns: Vec<ColumnId>) -> Result<ConstraintId> {
262        {
263            let t = self.table_mut(table)?;
264            for c in &columns {
265                let col = t.columns.get_mut(c).ok_or_else(|| Error::UnknownReference {
266                    kind: "column",
267                    id: format!("{}", c.0),
268                })?;
269                col.is_primary = true;
270                col.nullable = false;
271            }
272            let existing_pk: Vec<ConstraintId> = t
273                .constraints
274                .iter()
275                .filter_map(|(id, c)| c.is_primary_key().then_some(*id))
276                .collect();
277            for id in existing_pk {
278                t.constraints.shift_remove(&id);
279            }
280        }
281        let id = ConstraintId(self.mint());
282        let t = self.table_mut(table)?;
283        t.constraints
284            .insert(id, Constraint { id, name: None, kind: ConstraintKind::PrimaryKey { columns } });
285        Ok(id)
286    }
287
288    /// Add a foreign-key constraint on `table`.
289    pub fn add_foreign_key(&mut self, table: TableId, fk: ForeignKey) -> Result<ConstraintId> {
290        {
291            let t = self.table_mut(table)?;
292            for c in &fk.columns {
293                let col = t.columns.get_mut(c).ok_or_else(|| Error::UnknownReference {
294                    kind: "column",
295                    id: format!("{}", c.0),
296                })?;
297                col.is_foreign = true;
298            }
299        }
300        let id = ConstraintId(self.mint());
301        let t = self.table_mut(table)?;
302        t.constraints
303            .insert(id, Constraint { id, name: None, kind: ConstraintKind::ForeignKey(fk) });
304        Ok(id)
305    }
306
307    /// Add a UNIQUE constraint over the given columns.
308    pub fn add_unique(&mut self, table: TableId, columns: Vec<ColumnId>) -> Result<ConstraintId> {
309        let id = ConstraintId(self.mint());
310        let t = self.table_mut(table)?;
311        if columns.len() == 1 {
312            if let Some(col) = t.columns.get_mut(&columns[0]) {
313                col.is_unique = true;
314            }
315        }
316        t.constraints
317            .insert(id, Constraint { id, name: None, kind: ConstraintKind::Unique { columns } });
318        Ok(id)
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    #[test]
327    fn build_table_with_pk_and_fk() {
328        let mut m = LogicalModel::new("shop");
329        let customer = m.add_table("Customer");
330        let cid = m.add_column(customer, "id", DataType::Integer).unwrap();
331        m.add_column(customer, "name", DataType::Varchar(120)).unwrap();
332        m.set_primary_key(customer, vec![cid]).unwrap();
333
334        let order = m.add_table("Order");
335        let oid = m.add_column(order, "id", DataType::Integer).unwrap();
336        let ocust = m.add_column(order, "customer_id", DataType::Integer).unwrap();
337        m.set_primary_key(order, vec![oid]).unwrap();
338        m.add_foreign_key(
339            order,
340            ForeignKey {
341                columns: vec![ocust],
342                references_table: customer,
343                references_columns: vec![cid],
344                on_update: ReferentialAction::NoAction,
345                on_delete: ReferentialAction::NoAction,
346            },
347        )
348        .unwrap();
349
350        let order_t = m.table(order).unwrap();
351        assert_eq!(order_t.primary_key_columns(), &[oid]);
352        assert!(order_t.column(ocust).unwrap().is_foreign);
353        assert!(!order_t.column(ocust).unwrap().nullable);
354    }
355}