Skip to main content

remodel_core/models/
conceptual.rs

1//! Conceptual (Entity-Relationship) model.
2//!
3//! The conceptual model is a directed multigraph of entities, relationships,
4//! attributes, specializations, and unions. Elements are referenced by typed
5//! [`u32`] handles (à la ECS) rather than by `Rc`/`Arc`, which keeps the model
6//! easy to serialize, clone, and mutate without lifetime pain.
7//!
8//! ## Mapping from brModelo
9//!
10//! | brModelo class           | RemodelCore type                 |
11//! |--------------------------|----------------------------------|
12//! | `Entidade`               | [`Entity`]                       |
13//! | `Atributo`               | [`Attribute`]                    |
14//! | `Relacionamento`         | [`Relationship`]                 |
15//! | `Especializacao`         | [`Specialization`]               |
16//! | `Uniao`                  | [`Union`]                        |
17//! | `EntidadeAssociativa`    | [`AssociativeEntity`]            |
18//! | `Cardinalidade`          | [`crate::models::cardinality::Cardinality`] |
19//! | `DiagramaConceitual`     | [`ConceptualModel`]              |
20
21use indexmap::IndexMap;
22use serde::{Deserialize, Serialize};
23
24use crate::error::{Error, Result};
25use crate::models::cardinality::Cardinality;
26use crate::models::types::DataType;
27
28/// Strongly-typed handle for an [`Entity`] inside a [`ConceptualModel`].
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
30pub struct EntityId(pub u32);
31
32/// Strongly-typed handle for an [`Attribute`].
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
34pub struct AttributeId(pub u32);
35
36/// Strongly-typed handle for a [`Relationship`].
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
38pub struct RelationshipId(pub u32);
39
40/// Strongly-typed handle for a [`Specialization`].
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
42pub struct SpecializationId(pub u32);
43
44/// Strongly-typed handle for a [`Union`].
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
46pub struct UnionId(pub u32);
47
48/// Strongly-typed handle for an [`AssociativeEntity`].
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
50pub struct AssociativeEntityId(pub u32);
51
52/// What kind of attribute this is.
53///
54/// Mirrors brModelo's flags on `Atributo`. A single attribute can be both
55/// `Primary` and another kind in the original Java model (an attribute is
56/// identified by a `boolean isIdentificador`); here that is split into
57/// `is_primary` (a flag on [`Attribute`]) and `kind` (the structural kind).
58#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
59pub enum AttributeKind {
60    /// A plain single-valued attribute.
61    #[default]
62    Simple,
63    /// A composite attribute, made of nested sub-attributes.
64    Composite,
65    /// A multivalued attribute. The pair carries the cardinality bounds
66    /// `(min, max)`; `max == None` means unbounded.
67    Multivalued {
68        /// Minimum number of values; `0` makes the attribute optional.
69        min: u32,
70        /// Maximum number of values, or `None` for unbounded (`*`).
71        max: Option<u32>,
72    },
73    /// A derived attribute, computed from other attributes.
74    Derived,
75}
76
77/// An ER entity. Holds owned attributes and a name.
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct Entity {
80    /// Unique handle within the owning model.
81    pub id: EntityId,
82    /// Display name (e.g. `"Customer"`).
83    pub name: String,
84    /// Free-form note shown in the inspector panel.
85    pub note: String,
86    /// IDs of attributes owned by this entity, in author order.
87    pub attributes: Vec<AttributeId>,
88    /// `true` for *weak entities*, which require an identifying relationship
89    /// to be uniquely identified.
90    pub weak: bool,
91}
92
93/// An attribute belonging to either an [`Entity`] or a [`Relationship`].
94#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
95pub struct Attribute {
96    /// Unique handle within the owning model.
97    pub id: AttributeId,
98    /// Display name (e.g. `"first_name"`).
99    pub name: String,
100    /// Logical data type (mapped to a column type during conversion).
101    pub data_type: DataType,
102    /// Whether this attribute participates in the entity's primary identifier.
103    pub is_primary: bool,
104    /// Whether this attribute is a *partial* key — combined with the
105    /// owning entity's identifying relationship to form a key (typical for
106    /// weak entities).
107    pub is_partial_key: bool,
108    /// Whether the attribute admits a null value.
109    pub is_optional: bool,
110    /// Structural kind of the attribute.
111    pub kind: AttributeKind,
112    /// For composite attributes, the IDs of the sub-attributes that compose
113    /// this one. Empty for non-composite attributes.
114    pub children: Vec<AttributeId>,
115}
116
117/// One endpoint of a [`Relationship`].
118///
119/// The cardinality annotation follows the **"look-here"** convention used in
120/// Heuser's textbook and most modern UML tools: the cardinality next to
121/// entity *E* describes **how many tuples of the other entity** participate
122/// per tuple of *E*.
123///
124/// For example, in a `wrote` relationship between `Book` and `Author`,
125/// recording `Book.cardinality = ZeroToMany` means *"each book has 0..N
126/// authors"* (so `Author` is the many side from `Book`'s perspective).
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct RelationshipEndpoint {
129    /// The entity participating at this endpoint.
130    pub entity: EntityId,
131    /// How many tuples of the *other* entity participate per tuple of
132    /// `entity`. See the type-level docs for the convention.
133    pub cardinality: Cardinality,
134    /// Optional role label (brModelo's `Papel`).
135    pub role: Option<String>,
136}
137
138/// A relationship between two or more entities.
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
140pub struct Relationship {
141    /// Unique handle within the owning model.
142    pub id: RelationshipId,
143    /// Display name (e.g. `"works_for"`).
144    pub name: String,
145    /// Endpoints, in author order. A relationship is *binary* iff
146    /// `endpoints.len() == 2`, *self* iff all endpoints share an entity.
147    pub endpoints: Vec<RelationshipEndpoint>,
148    /// Attributes carried by the relationship itself (descriptive attributes).
149    pub attributes: Vec<AttributeId>,
150}
151
152impl Relationship {
153    /// `true` if every endpoint refers to the same entity.
154    pub fn is_self(&self) -> bool {
155        if self.endpoints.is_empty() {
156            return false;
157        }
158        let first = self.endpoints[0].entity;
159        self.endpoints.iter().all(|e| e.entity == first)
160    }
161
162    /// `true` for binary relationships (exactly 2 endpoints).
163    pub fn is_binary(&self) -> bool {
164        self.endpoints.len() == 2
165    }
166
167    /// `true` if at least three distinct entity endpoints participate.
168    pub fn is_nary(&self) -> bool {
169        self.endpoints.len() >= 3
170    }
171}
172
173/// Whether a specialization is total (every parent instance is in some child)
174/// or partial (some parent instances may be in no child), and whether it is
175/// disjoint (an instance can be in at most one child) or overlapping
176/// (an instance can be in several children).
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
178pub struct SpecializationKind {
179    /// `true` if every parent instance must belong to some child.
180    pub total: bool,
181    /// `true` if a parent instance may belong to multiple children at once.
182    pub overlapping: bool,
183}
184
185impl SpecializationKind {
186    /// Partial + disjoint: the most permissive variant (the default).
187    pub const PARTIAL_DISJOINT: Self = Self { total: false, overlapping: false };
188    /// Total + disjoint.
189    pub const TOTAL_DISJOINT: Self = Self { total: true, overlapping: false };
190    /// Partial + overlapping.
191    pub const PARTIAL_OVERLAPPING: Self = Self { total: false, overlapping: true };
192    /// Total + overlapping.
193    pub const TOTAL_OVERLAPPING: Self = Self { total: true, overlapping: true };
194}
195
196/// IS-A specialization (generalization) connecting one parent entity to
197/// several child entities. brModelo calls this `Especializacao`.
198#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
199pub struct Specialization {
200    /// Unique handle within the owning model.
201    pub id: SpecializationId,
202    /// Optional name shown next to the specialization gadget.
203    pub name: String,
204    /// The supertype.
205    pub parent: EntityId,
206    /// The subtypes.
207    pub children: Vec<EntityId>,
208    /// Discriminator flags.
209    pub kind: SpecializationKind,
210}
211
212/// Union/category construct: several parent entities feed a single category
213/// child entity (brModelo's `Uniao`).
214#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215pub struct Union {
216    /// Unique handle within the owning model.
217    pub id: UnionId,
218    /// Optional name shown next to the union gadget.
219    pub name: String,
220    /// Parent entities whose union forms `category`.
221    pub parents: Vec<EntityId>,
222    /// The category entity (the union of `parents`).
223    pub category: EntityId,
224}
225
226/// An associative entity wraps a relationship so that it can itself
227/// participate in further relationships (brModelo's `EntidadeAssociativa`).
228#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
229pub struct AssociativeEntity {
230    /// Unique handle within the owning model.
231    pub id: AssociativeEntityId,
232    /// The relationship that this associative entity wraps.
233    pub relationship: RelationshipId,
234    /// Display name; defaults to the wrapped relationship's name.
235    pub name: String,
236}
237
238/// The full conceptual model: entities, attributes, relationships, and the
239/// higher-level constructs (specialization, union, associative).
240///
241/// Element ordering is deterministic (author order) so that round-tripping
242/// through serialization preserves the diagram.
243#[derive(Debug, Clone, Default, Serialize, Deserialize)]
244pub struct ConceptualModel {
245    /// Diagram name (typically the project or schema name).
246    pub name: String,
247    /// Monotonic counter used to mint unique IDs.
248    next_id: u32,
249    /// Entities, keyed for O(1) lookup; iteration order is insertion order.
250    pub entities: IndexMap<EntityId, Entity>,
251    /// Attributes shared between entities and relationships.
252    pub attributes: IndexMap<AttributeId, Attribute>,
253    /// Relationships.
254    pub relationships: IndexMap<RelationshipId, Relationship>,
255    /// Specializations.
256    pub specializations: IndexMap<SpecializationId, Specialization>,
257    /// Unions / categories.
258    pub unions: IndexMap<UnionId, Union>,
259    /// Associative entities.
260    pub associative_entities: IndexMap<AssociativeEntityId, AssociativeEntity>,
261}
262
263impl ConceptualModel {
264    /// Create a new empty model with the given diagram name.
265    pub fn new(name: impl Into<String>) -> Self {
266        Self {
267            name: name.into(),
268            ..Self::default()
269        }
270    }
271
272    fn mint(&mut self) -> u32 {
273        self.next_id = self.next_id.checked_add(1).expect("ID space exhausted");
274        self.next_id
275    }
276
277    /// Add a new entity with the given name. Returns its handle.
278    pub fn add_entity(&mut self, name: impl Into<String>) -> EntityId {
279        let id = EntityId(self.mint());
280        self.entities.insert(
281            id,
282            Entity {
283                id,
284                name: name.into(),
285                note: String::new(),
286                attributes: Vec::new(),
287                weak: false,
288            },
289        );
290        id
291    }
292
293    /// Add a new *weak* entity. Functionally identical to
294    /// [`Self::add_entity`] but flips the `weak` flag, which downstream
295    /// conversion uses to require an identifying relationship.
296    pub fn add_weak_entity(&mut self, name: impl Into<String>) -> EntityId {
297        let id = self.add_entity(name);
298        self.entity_mut(id).expect("just inserted").weak = true;
299        id
300    }
301
302    /// Borrow an entity by its handle.
303    pub fn entity(&self, id: EntityId) -> Result<&Entity> {
304        self.entities
305            .get(&id)
306            .ok_or_else(|| Error::UnknownReference { kind: "entity", id: format!("{}", id.0) })
307    }
308
309    /// Mutably borrow an entity by its handle.
310    pub fn entity_mut(&mut self, id: EntityId) -> Result<&mut Entity> {
311        self.entities
312            .get_mut(&id)
313            .ok_or_else(|| Error::UnknownReference { kind: "entity", id: format!("{}", id.0) })
314    }
315
316    /// Add a new attribute and attach it to `owner`.
317    ///
318    /// `owner` may be either an [`EntityId`] or a [`RelationshipId`]; the
319    /// attribute is appended to that owner's attribute list.
320    pub fn add_attribute(
321        &mut self,
322        owner: AttributeOwner,
323        name: impl Into<String>,
324        data_type: DataType,
325    ) -> Result<AttributeId> {
326        let id = AttributeId(self.mint());
327        self.attributes.insert(
328            id,
329            Attribute {
330                id,
331                name: name.into(),
332                data_type,
333                is_primary: false,
334                is_partial_key: false,
335                is_optional: false,
336                kind: AttributeKind::Simple,
337                children: Vec::new(),
338            },
339        );
340        match owner {
341            AttributeOwner::Entity(eid) => self.entity_mut(eid)?.attributes.push(id),
342            AttributeOwner::Relationship(rid) => self.relationship_mut(rid)?.attributes.push(id),
343        }
344        Ok(id)
345    }
346
347    /// Convenience: add a primary-key attribute to an entity.
348    pub fn add_primary_attribute(
349        &mut self,
350        entity: EntityId,
351        name: impl Into<String>,
352        data_type: DataType,
353    ) -> Result<AttributeId> {
354        let id = self.add_attribute(AttributeOwner::Entity(entity), name, data_type)?;
355        let attr = self.attribute_mut(id)?;
356        attr.is_primary = true;
357        Ok(id)
358    }
359
360    /// Borrow an attribute by its handle.
361    pub fn attribute(&self, id: AttributeId) -> Result<&Attribute> {
362        self.attributes
363            .get(&id)
364            .ok_or_else(|| Error::UnknownReference { kind: "attribute", id: format!("{}", id.0) })
365    }
366
367    /// Mutably borrow an attribute.
368    pub fn attribute_mut(&mut self, id: AttributeId) -> Result<&mut Attribute> {
369        self.attributes
370            .get_mut(&id)
371            .ok_or_else(|| Error::UnknownReference { kind: "attribute", id: format!("{}", id.0) })
372    }
373
374    /// Start building a relationship. The returned [`RelationshipBuilder`]
375    /// records each endpoint and is finalized by dropping it (the relationship
376    /// is inserted into the model immediately on construction; `with` mutates
377    /// it in place).
378    pub fn relate(
379        &mut self,
380        name: impl Into<String>,
381        first: EntityId,
382        first_card: Cardinality,
383    ) -> RelationshipBuilder<'_> {
384        let id = RelationshipId(self.mint());
385        let rel = Relationship {
386            id,
387            name: name.into(),
388            endpoints: vec![RelationshipEndpoint {
389                entity: first,
390                cardinality: first_card,
391                role: None,
392            }],
393            attributes: Vec::new(),
394        };
395        self.relationships.insert(id, rel);
396        RelationshipBuilder { model: self, id }
397    }
398
399    /// Borrow a relationship by its handle.
400    pub fn relationship(&self, id: RelationshipId) -> Result<&Relationship> {
401        self.relationships.get(&id).ok_or_else(|| Error::UnknownReference {
402            kind: "relationship",
403            id: format!("{}", id.0),
404        })
405    }
406
407    /// Mutably borrow a relationship.
408    pub fn relationship_mut(&mut self, id: RelationshipId) -> Result<&mut Relationship> {
409        self.relationships.get_mut(&id).ok_or_else(|| Error::UnknownReference {
410            kind: "relationship",
411            id: format!("{}", id.0),
412        })
413    }
414
415    /// Add a new specialization.
416    pub fn add_specialization(
417        &mut self,
418        name: impl Into<String>,
419        parent: EntityId,
420        children: Vec<EntityId>,
421        kind: SpecializationKind,
422    ) -> Result<SpecializationId> {
423        if children.len() < 2 {
424            return Err(Error::InvalidSpecialization(format!(
425                "specialization `{}` must have at least 2 children, got {}",
426                name.into(),
427                children.len()
428            )));
429        }
430        let _ = self.entity(parent)?;
431        for c in &children {
432            let _ = self.entity(*c)?;
433        }
434        let id = SpecializationId(self.mint());
435        let name = name.into();
436        self.specializations.insert(id, Specialization { id, name, parent, children, kind });
437        Ok(id)
438    }
439
440    /// Add a new union/category construct.
441    pub fn add_union(
442        &mut self,
443        name: impl Into<String>,
444        parents: Vec<EntityId>,
445        category: EntityId,
446    ) -> Result<UnionId> {
447        if parents.len() < 2 {
448            return Err(Error::InvalidSpecialization(format!(
449                "union `{}` must have at least 2 parents, got {}",
450                name.into(),
451                parents.len()
452            )));
453        }
454        let _ = self.entity(category)?;
455        for p in &parents {
456            let _ = self.entity(*p)?;
457        }
458        let id = UnionId(self.mint());
459        self.unions
460            .insert(id, Union { id, name: name.into(), parents, category });
461        Ok(id)
462    }
463
464    /// Wrap a relationship as an associative entity.
465    pub fn add_associative_entity(
466        &mut self,
467        relationship: RelationshipId,
468    ) -> Result<AssociativeEntityId> {
469        let rel = self.relationship(relationship)?;
470        let name = rel.name.clone();
471        let id = AssociativeEntityId(self.mint());
472        self.associative_entities
473            .insert(id, AssociativeEntity { id, relationship, name });
474        Ok(id)
475    }
476}
477
478/// Owner reference passed to [`ConceptualModel::add_attribute`].
479#[derive(Debug, Clone, Copy)]
480pub enum AttributeOwner {
481    /// Attach the attribute to an entity.
482    Entity(EntityId),
483    /// Attach the attribute to a relationship (descriptive attribute).
484    Relationship(RelationshipId),
485}
486
487/// Fluent builder returned by [`ConceptualModel::relate`].
488///
489/// Use [`with`](Self::with) to add additional endpoints (binary, ternary, …)
490/// and [`carry`](Self::carry) to attach descriptive attributes.
491pub struct RelationshipBuilder<'m> {
492    model: &'m mut ConceptualModel,
493    id: RelationshipId,
494}
495
496impl<'m> RelationshipBuilder<'m> {
497    /// Add another endpoint to this relationship.
498    pub fn with(self, entity: EntityId, cardinality: Cardinality) -> Self {
499        if let Some(rel) = self.model.relationships.get_mut(&self.id) {
500            rel.endpoints.push(RelationshipEndpoint { entity, cardinality, role: None });
501        }
502        self
503    }
504
505    /// Set a role label on the most recently added endpoint.
506    pub fn with_role(self, role: impl Into<String>) -> Self {
507        if let Some(rel) = self.model.relationships.get_mut(&self.id) {
508            if let Some(last) = rel.endpoints.last_mut() {
509                last.role = Some(role.into());
510            }
511        }
512        self
513    }
514
515    /// Attach a descriptive attribute to the relationship.
516    pub fn carry(self, name: impl Into<String>, data_type: DataType) -> Self {
517        let _ = self
518            .model
519            .add_attribute(AttributeOwner::Relationship(self.id), name, data_type);
520        self
521    }
522
523    /// Finalize the builder and return the relationship's handle.
524    pub fn id(self) -> RelationshipId {
525        self.id
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532
533    fn sample() -> ConceptualModel {
534        let mut m = ConceptualModel::new("library");
535        let book = m.add_entity("Book");
536        let author = m.add_entity("Author");
537        m.add_primary_attribute(book, "id", DataType::Integer).unwrap();
538        m.add_attribute(AttributeOwner::Entity(book), "title", DataType::Varchar(255)).unwrap();
539        m.add_primary_attribute(author, "id", DataType::Integer).unwrap();
540        m.relate("wrote", book, Cardinality::ZeroToMany)
541            .with(author, Cardinality::OneToMany)
542            .id();
543        m
544    }
545
546    #[test]
547    fn build_basic_model() {
548        let m = sample();
549        assert_eq!(m.entities.len(), 2);
550        assert_eq!(m.attributes.len(), 3);
551        assert_eq!(m.relationships.len(), 1);
552        let rel = m.relationships.values().next().unwrap();
553        assert!(rel.is_binary());
554        assert!(!rel.is_self());
555    }
556
557    #[test]
558    fn relationship_self_check() {
559        let mut m = ConceptualModel::new("hr");
560        let person = m.add_entity("Person");
561        let rel = m
562            .relate("manages", person, Cardinality::ZeroToOne)
563            .with(person, Cardinality::ZeroToMany)
564            .id();
565        assert!(m.relationship(rel).unwrap().is_self());
566    }
567
568    #[test]
569    fn specialization_requires_two_children() {
570        let mut m = ConceptualModel::new("vehicles");
571        let v = m.add_entity("Vehicle");
572        let car = m.add_entity("Car");
573        let err = m
574            .add_specialization("kind", v, vec![car], SpecializationKind::PARTIAL_DISJOINT)
575            .unwrap_err();
576        assert!(matches!(err, Error::InvalidSpecialization(_)));
577    }
578
579    #[test]
580    fn unknown_entity_error() {
581        let m = ConceptualModel::new("x");
582        let err = m.entity(EntityId(99)).unwrap_err();
583        assert!(matches!(err, Error::UnknownReference { kind: "entity", .. }));
584    }
585
586    #[test]
587    fn json_round_trip() {
588        let m = sample();
589        let s = serde_json::to_string(&m).unwrap();
590        let back: ConceptualModel = serde_json::from_str(&s).unwrap();
591        assert_eq!(m.entities.len(), back.entities.len());
592        assert_eq!(m.attributes.len(), back.attributes.len());
593        assert_eq!(m.relationships.len(), back.relationships.len());
594    }
595}