Skip to content

[BUG] DBML Ref Endpoint Order #1054

Description

@ludengke95

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

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions