Skip to content

Commit 1388787

Browse files
committed
fix: use compositional syntax for aggregation relationships
on-behalf-of: @Mermaid-Chart <[email protected]>
1 parent 74a582b commit 1388787

File tree

5 files changed

+151
-70
lines changed

5 files changed

+151
-70
lines changed

cypress/integration/rendering/erDiagram.spec.js

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -463,9 +463,9 @@ ORDER ||--|{ LINE-ITEM : contains
463463
imgSnapshotTest(
464464
`
465465
erDiagram
466-
DEPARTMENT <> EMPLOYEE : contains
467-
PROJECT <>.. TASK : manages
468-
TEAM <> MEMBER : consists_of
466+
DEPARTMENT ||<>--|| EMPLOYEE : contains
467+
PROJECT o{<>..o{ TASK : manages
468+
TEAM ||<>--|| MEMBER : consists_of
469469
`,
470470
{ logLevel: 1 }
471471
);
@@ -475,7 +475,7 @@ ORDER ||--|{ LINE-ITEM : contains
475475
imgSnapshotTest(
476476
`
477477
erDiagram
478-
DEPARTMENT <> EMPLOYEE : contains
478+
DEPARTMENT ||<>--o{ EMPLOYEE : contains
479479
DEPARTMENT {
480480
int id PK
481481
string name
@@ -495,9 +495,9 @@ ORDER ||--|{ LINE-ITEM : contains
495495
imgSnapshotTest(
496496
`
497497
erDiagram
498-
UNIVERSITY <> COLLEGE : "has multiple"
499-
COLLEGE <> DEPARTMENT : "contains"
500-
DEPARTMENT <> FACULTY : "employs"
498+
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
499+
COLLEGE ||<>--o{ DEPARTMENT : "contains"
500+
DEPARTMENT ||<>--o{ FACULTY : "employs"
501501
`,
502502
{ logLevel: 1 }
503503
);
@@ -509,8 +509,8 @@ ORDER ||--|{ LINE-ITEM : contains
509509
erDiagram
510510
CUSTOMER ||--o{ ORDER : places
511511
ORDER ||--|{ ORDER_ITEM : contains
512-
PRODUCT <> ORDER_ITEM : "aggregated in"
513-
WAREHOUSE <>.. PRODUCT : "stores"
512+
PRODUCT ||<>--o{ ORDER_ITEM : "aggregated in"
513+
WAREHOUSE o{<>..o{ PRODUCT : "stores"
514514
`,
515515
{ logLevel: 1 }
516516
);
@@ -525,8 +525,8 @@ ORDER ||--|{ LINE-ITEM : contains
525525
p[PROJECT]
526526
t[TASK]
527527
528-
d <> e : contains
529-
p <>.. t : manages
528+
d ||<>--|| e : contains
529+
p o{<>..o{ t : manages
530530
531531
`,
532532
{ logLevel: 1 }
@@ -537,11 +537,34 @@ ORDER ||--|{ LINE-ITEM : contains
537537
imgSnapshotTest(
538538
`
539539
erDiagram
540-
COMPANY <> DEPARTMENT : owns
541-
DEPARTMENT <> EMPLOYEE : contains
542-
EMPLOYEE <> PROJECT : works_on
543-
PROJECT <> TASK : consists_of
544-
TASK <> SUBTASK : includes
540+
COMPANY ||<>--o{ DEPARTMENT : owns
541+
DEPARTMENT ||<>--o{ EMPLOYEE : contains
542+
EMPLOYEE o{<>--o{ PROJECT : works_on
543+
PROJECT ||<>--o{ TASK : consists_of
544+
TASK ||<>--o{ SUBTASK : includes
545+
`,
546+
{ logLevel: 1 }
547+
);
548+
});
549+
550+
it('should render aggregation with different cardinalities', () => {
551+
imgSnapshotTest(
552+
`
553+
erDiagram
554+
COMPANY ||<>--o{ DEPARTMENT : has
555+
MANAGER o|<>..o| TEAM : leads
556+
PRODUCT |{<>--|{ CATEGORY : belongs_to
557+
`,
558+
{ logLevel: 1 }
559+
);
560+
});
561+
562+
it('should render aggregation with zero-or-one relationships', () => {
563+
imgSnapshotTest(
564+
`
565+
erDiagram
566+
PERSON o|<>--o| PASSPORT : owns
567+
EMPLOYEE o|<>..o| PARKING_SPOT : assigned
545568
`,
546569
{ logLevel: 1 }
547570
);

docs/syntax/entityRelationshipDiagram.md

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -213,37 +213,67 @@ erDiagram
213213

214214
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.
215215

216-
| Value | Alias for | Description |
217-
| :---: | :------------------: | ------------------------------ |
218-
| <> | _aggregation_ | Basic aggregation (solid line) |
219-
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
216+
Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
217+
218+
**Syntax:**
219+
220+
```
221+
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
222+
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
223+
```
224+
225+
Where:
226+
227+
- `<>` is the aggregation marker
228+
- `--` represents a solid line (identifying relationship)
229+
- `..` represents a dashed line (non-identifying relationship)
230+
- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more)
220231

221232
**Examples:**
222233

223234
```mermaid-example
224235
erDiagram
225-
DEPARTMENT <> EMPLOYEE : contains
226-
PROJECT <>.. TASK : manages
227-
TEAM <> MEMBER : consists_of
236+
DEPARTMENT ||<>--o{ EMPLOYEE : contains
237+
PROJECT o{<>..o{ TASK : manages
238+
TEAM ||<>--|| MEMBER : consists_of
239+
COMPANY ||<>--o{ DEPARTMENT : owns
228240
```
229241

230242
```mermaid
231243
erDiagram
232-
DEPARTMENT <> EMPLOYEE : contains
233-
PROJECT <>.. TASK : manages
234-
TEAM <> MEMBER : consists_of
244+
DEPARTMENT ||<>--o{ EMPLOYEE : contains
245+
PROJECT o{<>..o{ TASK : manages
246+
TEAM ||<>--|| MEMBER : consists_of
247+
COMPANY ||<>--o{ DEPARTMENT : owns
235248
```
236249

237250
In these examples:
238251

239-
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
240-
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
241-
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
252+
- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
253+
- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
254+
- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation)
255+
- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation)
242256

243257
**Aggregation vs Association**
244258

245-
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
246-
- **Association** (`||--`, `}o--`): General relationship between entities
259+
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`)
260+
- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type
261+
262+
**Additional Examples:**
263+
264+
```mermaid-example
265+
erDiagram
266+
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
267+
MANAGER o|<>..o| TEAM : leads
268+
PERSON o|<>--o| PASSPORT : owns
269+
```
270+
271+
```mermaid
272+
erDiagram
273+
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
274+
MANAGER o|<>..o| TEAM : leads
275+
PERSON o|<>--o| PASSPORT : owns
276+
```
247277

248278
### Attributes
249279

packages/mermaid/src/diagrams/er/parser/erDiagram.jison

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ o\| return 'ZERO_OR_ONE';
7575
o\{ return 'ZERO_OR_MORE';
7676
\|\{ return 'ONE_OR_MORE';
7777
u(?=[\.\-\|]) return 'MD_PARENT';
78-
"<>.." return 'AGGREGATION_DASHED';
7978
"<>" return 'AGGREGATION';
8079
\.\. return 'NON_IDENTIFYING';
8180
\-\- return 'IDENTIFYING';
@@ -202,18 +201,7 @@ statement
202201
yy.addRelationship($1, $7, $3, $2);
203202
yy.setClass([$3], $5);
204203
}
205-
| entityName 'AGGREGATION' entityName COLON role
206-
{
207-
yy.addEntity($1);
208-
yy.addEntity($3);
209-
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION', cardB: 'ZERO_OR_MORE' });
210-
}
211-
| entityName 'AGGREGATION_DASHED' entityName COLON role
212-
{
213-
yy.addEntity($1);
214-
yy.addEntity($3);
215-
yy.addRelationship($1, $5, $3, { cardA: 'ZERO_OR_MORE', relType: 'AGGREGATION_DASHED', cardB: 'ZERO_OR_MORE' });
216-
}
204+
217205

218206
| title title_value { $$=$2.trim();yy.setAccTitle($$); }
219207
| acc_title acc_title_value { $$=$2.trim();yy.setAccTitle($$); }
@@ -324,13 +312,13 @@ relSpec
324312
;
325313

326314
aggregationRelSpec
327-
: 'AGGREGATION' cardinality cardinality
315+
: cardinality 'AGGREGATION' 'IDENTIFYING' cardinality
328316
{
329-
$$ = { cardA: $2, relType: $1, cardB: $3 };
317+
$$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION, cardB: $4 };
330318
}
331-
| 'AGGREGATION_DASHED' cardinality cardinality
319+
| cardinality 'AGGREGATION' 'NON_IDENTIFYING' cardinality
332320
{
333-
$$ = { cardA: $2, relType: $1, cardB: $3 };
321+
$$ = { cardA: $1, relType: yy.Aggregation.AGGREGATION_DASHED, cardB: $4 };
334322
}
335323
;
336324

@@ -345,8 +333,6 @@ cardinality
345333
relType
346334
: 'NON_IDENTIFYING' { $$ = yy.Identification.NON_IDENTIFYING; }
347335
| 'IDENTIFYING' { $$ = yy.Identification.IDENTIFYING; }
348-
| 'AGGREGATION' { $$ = yy.Aggregation.AGGREGATION; }
349-
| 'AGGREGATION_DASHED' { $$ = yy.Aggregation.AGGREGATION_DASHED; }
350336
;
351337

352338
role

packages/mermaid/src/diagrams/er/parser/erDiagram.spec.js

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,8 +1089,8 @@ describe('when parsing ER diagram it...', function () {
10891089
});
10901090

10911091
describe('aggregation relationships', function () {
1092-
it('should parse basic aggregation syntax', function () {
1093-
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
1092+
it('should parse basic aggregation syntax with solid line', function () {
1093+
erDiagram.parser.parse('erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains');
10941094
const rels = erDb.getRelationships();
10951095
expect(erDb.getEntities().size).toBe(2);
10961096
expect(rels.length).toBe(1);
@@ -1101,7 +1101,7 @@ describe('when parsing ER diagram it...', function () {
11011101
});
11021102

11031103
it('should parse dashed aggregation syntax', function () {
1104-
erDiagram.parser.parse('erDiagram\nPROJECT <>.. TASK : manages');
1104+
erDiagram.parser.parse('erDiagram\nPROJECT o{<>..o{ TASK : manages');
11051105
const rels = erDb.getRelationships();
11061106
expect(erDb.getEntities().size).toBe(2);
11071107
expect(rels.length).toBe(1);
@@ -1112,7 +1112,7 @@ describe('when parsing ER diagram it...', function () {
11121112
});
11131113

11141114
it('should parse aggregation with quoted labels', function () {
1115-
erDiagram.parser.parse('erDiagram\nUNIVERSITY <> COLLEGE : "has multiple"');
1115+
erDiagram.parser.parse('erDiagram\nUNIVERSITY ||<>--|| COLLEGE : "has multiple"');
11161116
const rels = erDb.getRelationships();
11171117
expect(erDb.getEntities().size).toBe(2);
11181118
expect(rels.length).toBe(1);
@@ -1122,7 +1122,7 @@ describe('when parsing ER diagram it...', function () {
11221122

11231123
it('should parse multiple aggregation relationships', function () {
11241124
erDiagram.parser.parse(
1125-
'erDiagram\nDEPARTMENT <> EMPLOYEE : contains\nPROJECT <>.. TASK : manages'
1125+
'erDiagram\nDEPARTMENT o{<>--o{ EMPLOYEE : contains\nPROJECT o{<>..o{ TASK : manages'
11261126
);
11271127
const rels = erDb.getRelationships();
11281128
expect(erDb.getEntities().size).toBe(4);
@@ -1132,7 +1132,7 @@ describe('when parsing ER diagram it...', function () {
11321132
});
11331133

11341134
it('should parse aggregation with entity aliases', function () {
1135-
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd <> e : contains');
1135+
erDiagram.parser.parse('erDiagram\nd[DEPARTMENT]\ne[EMPLOYEE]\nd ||<>--|| e : contains');
11361136
const rels = erDb.getRelationships();
11371137
expect(erDb.getEntities().size).toBe(2);
11381138
expect(rels.length).toBe(1);
@@ -1142,20 +1142,40 @@ describe('when parsing ER diagram it...', function () {
11421142
});
11431143

11441144
it('should validate aggregation relationships', function () {
1145-
erDiagram.parser.parse('erDiagram\nDEPARTMENT <> EMPLOYEE : contains');
1145+
erDiagram.parser.parse('erDiagram\nDEPARTMENT ||<>--|| EMPLOYEE : contains');
11461146
const rels = erDb.getRelationships();
11471147
expect(erDb.validateAggregationRelationship(rels[0].relSpec)).toBe(true);
11481148
});
11491149

11501150
it('should handle mixed relationship types', function () {
11511151
erDiagram.parser.parse(
1152-
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT <> ORDER_ITEM : "aggregated in"'
1152+
'erDiagram\nCUSTOMER ||--o{ ORDER : places\nPRODUCT ||<>--|| ORDER_ITEM : "aggregated in"'
11531153
);
11541154
const rels = erDb.getRelationships();
11551155
expect(erDb.getEntities().size).toBe(4);
11561156
expect(rels.length).toBe(2);
11571157
expect(rels[0].relSpec.relType).toBe(erDb.Identification.IDENTIFYING);
11581158
expect(rels[1].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
11591159
});
1160+
1161+
it('should parse aggregation with different cardinalities', function () {
1162+
erDiagram.parser.parse('erDiagram\nCOMPANY ||<>--o{ DEPARTMENT : has');
1163+
const rels = erDb.getRelationships();
1164+
expect(erDb.getEntities().size).toBe(2);
1165+
expect(rels.length).toBe(1);
1166+
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION);
1167+
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ONLY_ONE);
1168+
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_MORE);
1169+
});
1170+
1171+
it('should parse aggregation with zero-or-one cardinality', function () {
1172+
erDiagram.parser.parse('erDiagram\nMANAGER o|<>..o| TEAM : leads');
1173+
const rels = erDb.getRelationships();
1174+
expect(erDb.getEntities().size).toBe(2);
1175+
expect(rels.length).toBe(1);
1176+
expect(rels[0].relSpec.relType).toBe(erDb.Aggregation.AGGREGATION_DASHED);
1177+
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_ONE);
1178+
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.ZERO_OR_ONE);
1179+
});
11601180
});
11611181
});

packages/mermaid/src/docs/syntax/entityRelationshipDiagram.md

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -155,30 +155,52 @@ erDiagram
155155

156156
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.
157157

158-
| Value | Alias for | Description |
159-
| :---: | :------------------: | ------------------------------ |
160-
| <> | _aggregation_ | Basic aggregation (solid line) |
161-
| <>.. | _aggregation-dashed_ | Dashed aggregation line |
158+
Aggregation syntax follows a compositional pattern where you combine cardinality markers with the aggregation symbol (`<>`) and line type:
159+
160+
**Syntax:**
161+
162+
```
163+
<first-entity> <cardinalityA><>--<cardinalityB> <second-entity> : <relationship-label>
164+
<first-entity> <cardinalityA><>..<cardinalityB> <second-entity> : <relationship-label>
165+
```
166+
167+
Where:
168+
169+
- `<>` is the aggregation marker
170+
- `--` represents a solid line (identifying relationship)
171+
- `..` represents a dashed line (non-identifying relationship)
172+
- Cardinality markers can be: `||` (only one), `o|` (zero or one), `o{` (zero or more), `|{` (one or more)
162173

163174
**Examples:**
164175

165176
```mermaid-example
166177
erDiagram
167-
DEPARTMENT <> EMPLOYEE : contains
168-
PROJECT <>.. TASK : manages
169-
TEAM <> MEMBER : consists_of
178+
DEPARTMENT ||<>--o{ EMPLOYEE : contains
179+
PROJECT o{<>..o{ TASK : manages
180+
TEAM ||<>--|| MEMBER : consists_of
181+
COMPANY ||<>--o{ DEPARTMENT : owns
170182
```
171183

172184
In these examples:
173185

174-
- `DEPARTMENT <> EMPLOYEE` shows that a department contains employees (aggregation)
175-
- `PROJECT <>.. TASK` shows that a project manages tasks (dashed aggregation)
176-
- `TEAM <> MEMBER` shows that a team consists of members (aggregation)
186+
- `DEPARTMENT ||<>--o{ EMPLOYEE` shows that one department contains zero or more employees (solid aggregation)
187+
- `PROJECT o{<>..o{ TASK` shows that zero or more projects manage zero or more tasks (dashed aggregation)
188+
- `TEAM ||<>--|| MEMBER` shows that one team consists of one member (solid aggregation)
189+
- `COMPANY ||<>--o{ DEPARTMENT` shows that one company owns zero or more departments (solid aggregation)
177190

178191
**Aggregation vs Association**
179192

180-
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently
181-
- **Association** (`||--`, `}o--`): General relationship between entities
193+
- **Aggregation** (`<>`): "Has-a" relationship where parts can exist independently. The aggregation marker must be combined with cardinalities and line type (e.g., `||<>--o{`)
194+
- **Association** (`||--`, `}o--`): General relationship between entities with cardinality markers directly connected to line type
195+
196+
**Additional Examples:**
197+
198+
```mermaid-example
199+
erDiagram
200+
UNIVERSITY ||<>--o{ COLLEGE : "has multiple"
201+
MANAGER o|<>..o| TEAM : leads
202+
PERSON o|<>--o| PASSPORT : owns
203+
```
182204

183205
### Attributes
184206

0 commit comments

Comments
 (0)