drawdb-io / drawdb-io/drawdb

[BUG] DBML Ref Endpoint Order

Open
#1,054 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
39.5k
Forks
3.2k
Avg merge
10h 46m
Merged PRs (30d)
16

Description

# 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
}
```

Contributor guide

Open the contributing guide

Research direction

Inspect the implementation that loops over schema.refs and builds relationships; compare its endpoint handling with the three DBML examples in this issue. Done when reversed `<` references assign the FK and PK endpoints correctly and the undirected `-` case resolves the 1-1 side using primary-key or unique fields, with coverage for the listed cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
databases
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.