#1054·drawdb

[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:

dbml
Table users { id int [pk] }
Table posts { id int [pk] user_id int }

Case 1: > — FK written first, arrow points right

dbml
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

dbml
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)

dbml
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

mermaid
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| M

Fix Applied

javascript
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
}