fix: use compositional syntax for aggregation relationships

on-behalf-of: @Mermaid-Chart <hello@mermaidchart.com>
This commit is contained in:
darshanr0107
2025-12-10 18:09:41 +05:30
parent 74a582b1b2
commit 1388787ddc
5 changed files with 151 additions and 70 deletions

View File

@@ -463,9 +463,9 @@ ORDER ||--|{ LINE-ITEM : contains
imgSnapshotTest( imgSnapshotTest(
` `
erDiagram erDiagram
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--|| EMPLOYEE : contains
PROJECT <>.. TASK : manages PROJECT o{<>..o{ TASK : manages
TEAM <> MEMBER : consists_of TEAM ||<>--|| MEMBER : consists_of
`, `,
{ logLevel: 1 } { logLevel: 1 }
); );
@@ -475,7 +475,7 @@ ORDER ||--|{ LINE-ITEM : contains
imgSnapshotTest( imgSnapshotTest(
` `
erDiagram erDiagram
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--o{ EMPLOYEE : contains
DEPARTMENT { DEPARTMENT {
int id PK int id PK
string name string name
@@ -495,9 +495,9 @@ ORDER ||--|{ LINE-ITEM : contains
imgSnapshotTest( imgSnapshotTest(
` `
erDiagram erDiagram
UNIVERSITY <> COLLEGE : "has multiple" UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
COLLEGE <> DEPARTMENT : "contains" COLLEGE ||<>--o{ DEPARTMENT : "contains"
DEPARTMENT <> FACULTY : "employs" DEPARTMENT ||<>--o{ FACULTY : "employs"
`, `,
{ logLevel: 1 } { logLevel: 1 }
); );
@@ -509,8 +509,8 @@ ORDER ||--|{ LINE-ITEM : contains
erDiagram erDiagram
CUSTOMER ||--o{ ORDER : places CUSTOMER ||--o{ ORDER : places
ORDER ||--|{ ORDER_ITEM : contains ORDER ||--|{ ORDER_ITEM : contains
PRODUCT <> ORDER_ITEM : "aggregated in" PRODUCT ||<>--o{ ORDER_ITEM : "aggregated in"
WAREHOUSE <>.. PRODUCT : "stores" WAREHOUSE o{<>..o{ PRODUCT : "stores"
`, `,
{ logLevel: 1 } { logLevel: 1 }
); );
@@ -525,8 +525,8 @@ ORDER ||--|{ LINE-ITEM : contains
p[PROJECT] p[PROJECT]
t[TASK] t[TASK]
d <> e : contains d ||<>--|| e : contains
p <>.. t : manages p o{<>..o{ t : manages
`, `,
{ logLevel: 1 } { logLevel: 1 }
@@ -537,11 +537,34 @@ ORDER ||--|{ LINE-ITEM : contains
imgSnapshotTest( imgSnapshotTest(
` `
erDiagram erDiagram
COMPANY <> DEPARTMENT : owns COMPANY ||<>--o{ DEPARTMENT : owns
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--o{ EMPLOYEE : contains
EMPLOYEE <> PROJECT : works_on EMPLOYEE o{<>--o{ PROJECT : works_on
PROJECT <> TASK : consists_of PROJECT ||<>--o{ TASK : consists_of
TASK <> SUBTASK : includes 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 } { logLevel: 1 }
); );

View File

@@ -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. 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 syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
| :---: | :------------------: | ------------------------------ |
| <> | _aggregation_ | Basic aggregation (solid line) | **Syntax:**
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
```
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
```
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:** **Examples:**
```mermaid-example ```mermaid-example
erDiagram erDiagram
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--o{ EMPLOYEE : contains
PROJECT <>.. TASK : manages PROJECT o{<>..o{ TASK : manages
TEAM <> MEMBER : consists_of TEAM ||<>--|| MEMBER : consists_of
COMPANY ||<>--o{ DEPARTMENT : owns
``` ```
```mermaid ```mermaid
erDiagram erDiagram
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--o{ EMPLOYEE : contains
PROJECT <>.. TASK : manages PROJECT o{<>..o{ TASK : manages
TEAM <> MEMBER : consists_of TEAM ||<>--|| MEMBER : consists_of
COMPANY ||<>--o{ DEPARTMENT : owns
``` ```
In these examples: In these examples:
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation) - `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation) - `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
- `TEAM <> MEMBER` shows that a team consists of members (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 vs Association**
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently - **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 - **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 ### Attributes

View File

@@ -75,7 +75,6 @@ o\| return 'ZERO_OR_ONE';
o\{ return 'ZERO_OR_MORE'; o\{ return 'ZERO_OR_MORE';
\|\{ return 'ONE_OR_MORE'; \|\{ return 'ONE_OR_MORE';
u(?=[\.\-\|]) return 'MD_PARENT'; u(?=[\.\-\|]) return 'MD_PARENT';
"<>.." return 'AGGREGATION_DASHED';
"<>" return 'AGGREGATION'; "<>" return 'AGGREGATION';
\.\. return 'NON_IDENTIFYING'; \.\. return 'NON_IDENTIFYING';
\-\- return 'IDENTIFYING'; \-\- return 'IDENTIFYING';
@@ -202,18 +201,7 @@ statement
yy.addRelationship($1, $7, $3, $2); yy.addRelationship($1, $7, $3, $2);
yy.setClass([$3], $5); 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($$); } | title title_value { $$=$2.trim();yy.setAccTitle($$); }
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); } | acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
@@ -324,13 +312,13 @@ relSpec
; ;
aggregationRelSpec 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 relType
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; } : 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; } | 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
| 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; }
| 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; }
; ;
role role

View File

@@ -1089,8 +1089,8 @@ describe('when parsing ER diagram it...', function () {
}); });
describe('aggregation relationships', function () { describe('aggregation relationships', function () {
it('should parse basic aggregation syntax', function () { it('should parse basic aggregation syntax with solid line', function () {
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains'); erDiagram.parser.parse('erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains');
const rels = erDb.getRelationships(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2); expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1); expect(rels.length).toBe(1);
@@ -1101,7 +1101,7 @@ describe('when parsing ER diagram it...', function () {
}); });
it('should parse dashed aggregation syntax', 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(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2); expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1); expect(rels.length).toBe(1);
@@ -1112,7 +1112,7 @@ describe('when parsing ER diagram it...', function () {
}); });
it('should parse aggregation with quoted labels', 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(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2); expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1); expect(rels.length).toBe(1);
@@ -1122,7 +1122,7 @@ describe('when parsing ER diagram it...', function () {
it('should parse multiple aggregation relationships', function () { it('should parse multiple aggregation relationships', function () {
erDiagram.parser.parse( 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(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(4); 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 () { 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(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(2); expect(erDb.getEntities().size).toBe(2);
expect(rels.length).toBe(1); expect(rels.length).toBe(1);
@@ -1142,14 +1142,14 @@ describe('when parsing ER diagram it...', function () {
}); });
it('should validate aggregation relationships', 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(); const rels = erDb.getRelationships();
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true); expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
}); });
it('should handle mixed relationship types', function () { it('should handle mixed relationship types', function () {
erDiagram.parser.parse( 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(); const rels = erDb.getRelationships();
expect(erDb.getEntities().size).toBe(4); 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[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION); 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);
});
}); });
}); });

View File

@@ -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. 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 syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
| :---: | :------------------: | ------------------------------ |
| <> | _aggregation_ | Basic aggregation (solid line) | **Syntax:**
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
```
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
```
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:** **Examples:**
```mermaid-example ```mermaid-example
erDiagram erDiagram
DEPARTMENT <> EMPLOYEE : contains DEPARTMENT ||<>--o{ EMPLOYEE : contains
PROJECT <>.. TASK : manages PROJECT o{<>..o{ TASK : manages
TEAM <> MEMBER : consists_of TEAM ||<>--|| MEMBER : consists_of
COMPANY ||<>--o{ DEPARTMENT : owns
``` ```
In these examples: In these examples:
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation) - `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation) - `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
- `TEAM <> MEMBER` shows that a team consists of members (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 vs Association**
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently - **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 - **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 ### Attributes