diff --git a/cypress/integration/rendering/erDiagram.spec.js b/cypress/integration/rendering/erDiagram.spec.js index 44b36a7f0..4e7e940d4 100644 --- a/cypress/integration/rendering/erDiagram.spec.js +++ b/cypress/integration/rendering/erDiagram.spec.js @@ -463,9 +463,9 @@ ORDER ||--|{ LINE-ITEM : contains imgSnapshotTest( ` erDiagram - DEPARTMENT <> EMPLOYEE : contains - PROJECT <>.. TASK : manages - TEAM <> MEMBER : consists_of + DEPARTMENT ||<>--|| EMPLOYEE : contains + PROJECT o{<>..o{ TASK : manages + TEAM ||<>--|| MEMBER : consists_of `, { logLevel: 1 } ); @@ -475,7 +475,7 @@ ORDER ||--|{ LINE-ITEM : contains imgSnapshotTest( ` erDiagram - DEPARTMENT <> EMPLOYEE : contains + DEPARTMENT ||<>--o{ EMPLOYEE : contains DEPARTMENT { int id PK string name @@ -495,9 +495,9 @@ ORDER ||--|{ LINE-ITEM : contains imgSnapshotTest( ` erDiagram - UNIVERSITY <> COLLEGE : "has multiple" - COLLEGE <> DEPARTMENT : "contains" - DEPARTMENT <> FACULTY : "employs" + UNIVERSITY ||<>--o{ COLLEGE : "has multiple" + COLLEGE ||<>--o{ DEPARTMENT : "contains" + DEPARTMENT ||<>--o{ FACULTY : "employs" `, { logLevel: 1 } ); @@ -509,8 +509,8 @@ ORDER ||--|{ LINE-ITEM : contains erDiagram CUSTOMER ||--o{ ORDER : places ORDER ||--|{ ORDER_ITEM : contains - PRODUCT <> ORDER_ITEM : "aggregated in" - WAREHOUSE <>.. PRODUCT : "stores" + PRODUCT ||<>--o{ ORDER_ITEM : "aggregated in" + WAREHOUSE o{<>..o{ PRODUCT : "stores" `, { logLevel: 1 } ); @@ -525,8 +525,8 @@ ORDER ||--|{ LINE-ITEM : contains p[PROJECT] t[TASK] - d <> e : contains - p <>.. t : manages + d ||<>--|| e : contains + p o{<>..o{ t : manages `, { logLevel: 1 } @@ -537,11 +537,34 @@ ORDER ||--|{ LINE-ITEM : contains imgSnapshotTest( ` erDiagram - COMPANY <> DEPARTMENT : owns - DEPARTMENT <> EMPLOYEE : contains - EMPLOYEE <> PROJECT : works_on - PROJECT <> TASK : consists_of - TASK <> SUBTASK : includes + COMPANY ||<>--o{ DEPARTMENT : owns + DEPARTMENT ||<>--o{ EMPLOYEE : contains + EMPLOYEE o{<>--o{ PROJECT : works_on + PROJECT ||<>--o{ TASK : consists_of + TASK ||<>--o{ SUBTASK : includes + `, + { logLevel: 1 } + ); + }); + + it('should render aggregation with different cardinalities', () => { + imgSnapshotTest( + ` + erDiagram + COMPANY ||<>--o{ DEPARTMENT : has + MANAGER o|<>..o| TEAM : leads + PRODUCT |{<>--|{ CATEGORY : belongs_to + `, + { logLevel: 1 } + ); + }); + + it('should render aggregation with zero-or-one relationships', () => { + imgSnapshotTest( + ` + erDiagram + PERSON o|<>--o| PASSPORT : owns + EMPLOYEE o|<>..o| PARKING_SPOT : assigned `, { logLevel: 1 } ); diff --git a/docs/syntax/entityRelationshipDiagram.md b/docs/syntax/entityRelationshipDiagram.md index b64f35f8b..bf9024ce3 100644 --- a/docs/syntax/entityRelationshipDiagram.md +++ b/docs/syntax/entityRelationshipDiagram.md @@ -213,37 +213,67 @@ erDiagram Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints. -| Value | Alias for | Description | -| :---: | :------------------: | ------------------------------ | -| <> | _aggregation_ | Basic aggregation (solid line) | -| <>.. | _aggregation-dashed_ | Dashed aggregation line | +Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type: + +**Syntax:** + +``` + <>-- : + <>.. : +``` + +Where: + +- `<>` is the aggregation marker +- `--` represents a solid line (identifying relationship) +- `..` represents a dashed line (non-identifying relationship) +- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more) **Examples:** ```mermaid-example erDiagram - DEPARTMENT <> EMPLOYEE : contains - PROJECT <>.. TASK : manages - TEAM <> MEMBER : consists_of + DEPARTMENT ||<>--o{ EMPLOYEE : contains + PROJECT o{<>..o{ TASK : manages + TEAM ||<>--|| MEMBER : consists_of + COMPANY ||<>--o{ DEPARTMENT : owns ``` ```mermaid erDiagram - DEPARTMENT <> EMPLOYEE : contains - PROJECT <>.. TASK : manages - TEAM <> MEMBER : consists_of + DEPARTMENT ||<>--o{ EMPLOYEE : contains + PROJECT o{<>..o{ TASK : manages + TEAM ||<>--|| MEMBER : consists_of + COMPANY ||<>--o{ DEPARTMENT : owns ``` In these examples: -- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation) -- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation) -- `TEAM <> MEMBER` shows that a team consists of members (aggregation) +- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation) +- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation) +- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation) +- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation) **Aggregation vs Association** -- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently -- **Association** (`||--`, `}o--`): General relationship between entities +- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`) +- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type + +**Additional Examples:** + +```mermaid-example +erDiagram + UNIVERSITY ||<>--o{ COLLEGE : "has multiple" + MANAGER o|<>..o| TEAM : leads + PERSON o|<>--o| PASSPORT : owns +``` + +```mermaid +erDiagram + UNIVERSITY ||<>--o{ COLLEGE : "has multiple" + MANAGER o|<>..o| TEAM : leads + PERSON o|<>--o| PASSPORT : owns +``` ### Attributes diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison index 51bde4a24..18d50be75 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.jison +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.jison @@ -75,7 +75,6 @@ o\| return 'ZERO_OR_ONE'; o\{ return 'ZERO_OR_MORE'; \|\{ return 'ONE_OR_MORE'; u(?=[\.\-\|]) return 'MD_PARENT'; -"<>.." return 'AGGREGATION_DASHED'; "<>" return 'AGGREGATION'; \.\. return 'NON_IDENTIFYING'; \-\- return 'IDENTIFYING'; @@ -202,18 +201,7 @@ statement yy.addRelationship($1, $7, $3, $2); yy.setClass([$3], $5); } - | entityName 'AGGREGATION' entityName COLON role - { - yy.addEntity($1); - yy.addEntity($3); - yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION', cardB: 'ZERO_OR_MORE' }); - } - | entityName 'AGGREGATION_DASHED' entityName COLON role - { - yy.addEntity($1); - yy.addEntity($3); - yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION_DASHED', cardB: 'ZERO_OR_MORE' }); - } + | title title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } @@ -324,13 +312,13 @@ relSpec ; aggregationRelSpec - : 'AGGREGATION' cardinality cardinality + : cardinality 'AGGREGATION' 'IDENTIFYING' cardinality { - $$ = { cardA: $2, relType: $1, cardB: $3 }; + $$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION, cardB: $4 }; } - | 'AGGREGATION_DASHED' cardinality cardinality + | cardinality 'AGGREGATION' 'NON_IDENTIFYING' cardinality { - $$ = { cardA: $2, relType: $1, cardB: $3 }; + $$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION_DASHED, cardB: $4 }; } ; @@ -345,8 +333,6 @@ cardinality relType : 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; } | 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; } - | 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; } - | 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; } ; role diff --git a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js index 4c466b396..30df22df9 100644 --- a/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js +++ b/packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js @@ -1089,8 +1089,8 @@ describe('when parsing ER diagram it...', function () { }); describe('aggregation relationships', function () { - it('should parse basic aggregation syntax', function () { - erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains'); + it('should parse basic aggregation syntax with solid line', function () { + erDiagram.parser.parse('erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains'); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(2); expect(rels.length).toBe(1); @@ -1101,7 +1101,7 @@ describe('when parsing ER diagram it...', function () { }); it('should parse dashed aggregation syntax', function () { - erDiagram.parser.parse('erDiagram\nPROJECT <>.. TASK : manages'); + erDiagram.parser.parse('erDiagram\nPROJECT o{<>..o{ TASK : manages'); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(2); expect(rels.length).toBe(1); @@ -1112,7 +1112,7 @@ describe('when parsing ER diagram it...', function () { }); it('should parse aggregation with quoted labels', function () { - erDiagram.parser.parse('erDiagram\nUNIVERSITY <> COLLEGE : "has multiple"'); + erDiagram.parser.parse('erDiagram\nUNIVERSITY ||<>--|| COLLEGE : "has multiple"'); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(2); expect(rels.length).toBe(1); @@ -1122,7 +1122,7 @@ describe('when parsing ER diagram it...', function () { it('should parse multiple aggregation relationships', function () { erDiagram.parser.parse( - 'erDiagram\nDEPARTMENT <> EMPLOYEE : contains\nPROJECT <>.. TASK : manages' + 'erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains\nPROJECT o{<>..o{ TASK : manages' ); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(4); @@ -1132,7 +1132,7 @@ describe('when parsing ER diagram it...', function () { }); it('should parse aggregation with entity aliases', function () { - erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd <> e : contains'); + erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd ||<>--|| e : contains'); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(2); expect(rels.length).toBe(1); @@ -1142,14 +1142,14 @@ describe('when parsing ER diagram it...', function () { }); it('should validate aggregation relationships', function () { - erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains'); + erDiagram.parser.parse('erDiagram\nDEPARTMENT ||<>--|| EMPLOYEE : contains'); const rels = erDb.getRelationships(); expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true); }); it('should handle mixed relationship types', function () { erDiagram.parser.parse( - 'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT <> ORDER_ITEM : "aggregated in"' + 'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT ||<>--|| ORDER_ITEM : "aggregated in"' ); const rels = erDb.getRelationships(); expect(erDb.getEntities().size).toBe(4); @@ -1157,5 +1157,25 @@ describe('when parsing ER diagram it...', function () { expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING); expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION); }); + + it('should parse aggregation with different cardinalities', function () { + erDiagram.parser.parse('erDiagram\nCOMPANY ||<>--o{ DEPARTMENT : has'); + const rels = erDb.getRelationships(); + expect(erDb.getEntities().size).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE); + }); + + it('should parse aggregation with zero-or-one cardinality', function () { + erDiagram.parser.parse('erDiagram\nMANAGER o|<>..o| TEAM : leads'); + const rels = erDb.getRelationships(); + expect(erDb.getEntities().size).toBe(2); + expect(rels.length).toBe(1); + expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED); + expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE); + expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE); + }); }); }); diff --git a/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md b/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md index a2e1ebfbd..cf95bf6ec 100644 --- a/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md +++ b/packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md @@ -155,30 +155,52 @@ erDiagram Aggregation represents a "has-a" relationship where the part can exist independently of the whole. This is different from composition, where the part cannot exist without the whole. Aggregation relationships are rendered with hollow diamond markers at the endpoints. -| Value | Alias for | Description | -| :---: | :------------------: | ------------------------------ | -| <> | _aggregation_ | Basic aggregation (solid line) | -| <>.. | _aggregation-dashed_ | Dashed aggregation line | +Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type: + +**Syntax:** + +``` + <>-- : + <>.. : +``` + +Where: + +- `<>` is the aggregation marker +- `--` represents a solid line (identifying relationship) +- `..` represents a dashed line (non-identifying relationship) +- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more) **Examples:** ```mermaid-example erDiagram - DEPARTMENT <> EMPLOYEE : contains - PROJECT <>.. TASK : manages - TEAM <> MEMBER : consists_of + DEPARTMENT ||<>--o{ EMPLOYEE : contains + PROJECT o{<>..o{ TASK : manages + TEAM ||<>--|| MEMBER : consists_of + COMPANY ||<>--o{ DEPARTMENT : owns ``` In these examples: -- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation) -- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation) -- `TEAM <> MEMBER` shows that a team consists of members (aggregation) +- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation) +- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation) +- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation) +- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation) **Aggregation vs Association** -- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently -- **Association** (`||--`, `}o--`): General relationship between entities +- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`) +- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type + +**Additional Examples:** + +```mermaid-example +erDiagram + UNIVERSITY ||<>--o{ COLLEGE : "has multiple" + MANAGER o|<>..o| TEAM : leads + PERSON o|<>--o| PASSPORT : owns +``` ### Attributes