[BUG] DBML Ref Endpoint Order
Author: ludengke95Created Jun 24, 2026Updated Jun 24, 2026
DBML Ref Endpoint Order Analysis
Behavior of @dbml/core Parser
The DBML parser assigns endpoints based on the order fields appear in the ref statement, NOT the ref direction symbol (>, <). The relation property (* or 1) is derived from the ref direction, but endpoint order is always lexical.
Test Cases
Test schema:
Table users { id int [pk] }
Table posts { id int [pk] user_id int }Case 1: > — FK written first, arrow points right
Ref: posts.user_id > users.id| Endpoint | tableName | fieldNames | relation |
|---|---|---|---|
| ep0 | posts | [user_id] | * |
| ep1 | users | [id] | 1 |
Problem: Current code treats ep0 as start (FK table), ep1 as end (PK table). This is correct here.
Case 2: < — FK written second, arrow points left
Ref: users.id < posts.user_id| Endpoint | tableName | fieldNames | relation |
|---|---|---|---|
| ep0 | users | [id] | 1 |
| ep1 | posts | [user_id] | * |
Problem: Current code treats ep0 as start (FK table). But ep0 = users (PK table) and ep1 = posts (FK table). The FK is in ep1, not ep0. The start = FK table rule is broken.
Case 3: - — No direction (1-1)
Ref: users.id - profiles.user_id| Endpoint | tableName | fieldNames | relation |
|---|---|---|---|
| ep0 | users | [id] | 1 |
| ep1 | profiles | [user_id] | 1 |
Problem: Both are 1. Which one is the FK? Must determine by checking if one side's fields are all primary keys or unique indexes.
Database Ref Direction Repair Logic
flowchart TD
A["For each ref in schema.refs\nep0 = ref.endpoints[0]\nep1 = ref.endpoints[1]"]
B{"ep0.relation === '*' ?"}
C["fkEndpoint = ep0\npkEndpoint = ep1"]
D{"ep1.relation === '*' ?"}
E["fkEndpoint = ep1\npkEndpoint = ep0"]
F["1-1: Check PK/unique\nAre ALL fields in ep0\nprimary or unique?"]
G{"ep0 all PK/unique AND\nep1 NOT all PK/unique?"}
H["fkEndpoint = ep1\npkEndpoint = ep0"]
I["default:\nfkEndpoint = ep1\npkEndpoint = ep0"]
J["start = fkEndpoint\nend = pkEndpoint"]
K{"fkRelation === '*' &&\npkRelation === '1' ?"}
L["MANY_TO_ONE"]
M["ONE_TO_ONE"]
A --> B
B -->|YES| C
B -->|NO| D
D -->|YES| E
D -->|NO, both are '1'| F
F --> G
G -->|YES| H
G -->|NO| I
C --> J
E --> J
H --> J
I --> J
J --> K
K -->|YES| L
K -->|NO| MFix Applied
for (const ref of schema.refs) {
const ep0 = ref.endpoints[0];
const ep1 = ref.endpoints[1];
// Determine FK side by relation: "*" = FK side, "1" = referenced side
let fkEndpoint, pkEndpoint;
if (ep0.relation === "*") {
fkEndpoint = ep0;
pkEndpoint = ep1;
} else if (ep1.relation === "*") {
fkEndpoint = ep1;
pkEndpoint = ep0;
} else {
// 1-1: check which side's fields are all PK/unique
const tablesMap = new Map(tables.map((t) => [t.name, t]));
const pkTable = tablesMap.get(ep0.tableName);
const fkTable = tablesMap.get(ep1.tableName);
if (pkTable && fkTable) {
const ep0FieldsAllPK = ep0.fieldNames.every((name) => {
const f = pkTable.fields.find((f) => f.name === name);
return f && (f.primary || f.unique);
});
const ep1FieldsAllPK = ep1.fieldNames.every((name) => {
const f = fkTable.fields.find((f) => f.name === name);
return f && (f.primary || f.unique);
});
if (ep0FieldsAllPK && !ep1FieldsAllPK) {
fkEndpoint = ep1;
pkEndpoint = ep0;
} else {
fkEndpoint = ep1;
pkEndpoint = ep0;
}
} else {
fkEndpoint = ep1;
pkEndpoint = ep0;
}
}
const startTableName = fkEndpoint.tableName;
const endTableName = pkEndpoint.tableName;
// ... rest of relationship building using fkEndpoint/pkEndpoint
}Source: drawdb-io/drawdb