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(
`
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 }
);

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.
| 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:**
```
<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:**
```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

View File

@@ -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

View File

@@ -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);
});
});
});

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.
| 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:**
```
<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:**
```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