drizzle-team / drizzle-team/drizzle-orm

[BUG]: drizzle-kit pull generates a many-to-many whose junction table is one of its own endpoints, and drops the one-relations the foreign keys describe

Open
#6,197 0 comments 0 reactions 0 assignees View on GitHub
bug drizzle/kit
Dominant language
TypeScript
Stars
35.8k
Forks
1.6k
Avg merge
2d 7h
Merged PRs (30d)
4

Description

### Report hasn't been filed before.

- [x] I have verified that the bug I'm about to report hasn't been filed before.

### What version of `drizzle-orm` are you using?

1.0.0-rc.4 (also reproduced on 1.0.0-rc.5-169397b)

### What version of `drizzle-kit` are you using?

1.0.0-rc.4 (also reproduced on 1.0.0-rc.5-ab785fc)

### Other packages

mysql2@3.24.2

### Describe the Bug

**`drizzle-kit pull` generates a many-to-many whose junction table is one of its own two endpoints, and drops the two `one` relations the foreign keys actually describe.**

MySQL 8.0.35 and 8.0.42, `mysql2` driver, Node 24, no monorepo needed.

#### Steps to reproduce

Two tables. `users` has exactly two outbound foreign keys: one to `organizations`, one **to itself**.

```sql
CREATE TABLE `organizations` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `users` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`organization_id` bigint unsigned DEFAULT NULL,
`referrer_id` bigint unsigned DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `users_organization_id_foreign` (`organization_id`),
KEY `users_referrer_id_foreign` (`referrer_id`),
CONSTRAINT `users_organization_id_foreign` FOREIGN KEY (`organization_id`) REFERENCES `organizations` (`id`),
CONSTRAINT `users_referrer_id_foreign` FOREIGN KEY (`referrer_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```

```ts
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';

export default defineConfig({
dialect: 'mysql',
schema: './src/schema.ts',
out: './src',
dbCredentials: { url: 'mysql://root:repro@127.0.0.1:3306/repro' },
});
```

```
drizzle-kit pull
```

#### What comes out

`src/schema.ts` is correct — both foreign keys are there and match `SHOW CREATE TABLE` exactly:

```ts
export const organizations = mysqlTable("organizations", {
id: bigint({ unsigned: true, mode: 'number' }).autoincrement().primaryKey(),
name: varchar({ length: 255 }).notNull(),
});

export const users = mysqlTable("users", {
id: bigint({ unsigned: true, mode: 'number' }).autoincrement().primaryKey(),
organizationId: bigint("organization_id", { unsigned: true, mode: 'number' }).references(() => organizations.id),
referrerId: bigint("referrer_id", { unsigned: true, mode: 'number' }),
},
(table) => [
foreignKey({
columns: [table.referrerId],
foreignColumns: [table.id],
name: "users_referrer_id_foreign"
}),
]);
```

`src/relations.ts` is not:

```ts
import { defineRelations } from "drizzle-orm";
import * as schema from "./schema";

export const relations = defineRelations(schema, (r) => ({
organizations: {
users: r.many.users({
from: r.organizations.id.through(r.users.organizationId),
to: r.users.id.through(r.users.referrerId)
}),
},
users: {
organizations: r.many.organizations(),
},
}))
```

#### Why it is wrong

Per the [many-to-many docs](https://rqbv2.drizzle-orm-fe.pages.dev/docs/relations-v2), `from: A.id.through(J.aId), to: B.id.through(J.bId)` means "A to B through junction J". Here the generator has picked **`users` as the junction between `organizations` and `users`**, chaining `organizations.id → users.organization_id`, then `users.referrer_id → users.id`.

So `db.query.organizations.findMany({ with: { users: true } })` does not answer *"who are this organization's members"*. It answers *"who was referred by this organization's members"* — a different set, and on our data a mostly empty one. There is no junction table in this schema; there are two unrelated foreign keys, one of which is self-referencing.

Two further consequences:

1. **`users.organizations` is a `many`**, but `organization_id` is a single nullable column — a user belongs to at most one organization.
2. **Both real relations are gone.** Nothing in the generated file expresses `users.organization_id → organizations.id` as a `one`, and the self-reference `users.referrer_id → users.id` is not expressed at all. They were consumed into the invented many-to-many rather than emitted alongside it.

#### The shape of the trigger

It is not specific to `users`. On a 29-table pull of the same database, every table whose outbound foreign keys are exactly two gets collapsed into a junction and loses its own `one` relations — including the ones that really are junctions (`bsessions`, `cards`, `station_permissions`, `purchase_corrections`, `shelf_capacities`), which no longer carry a way to read the junction row itself. `users` is the case where the heuristic produces a relation the foreign keys cannot support at all, because the "junction" is also the target.

#### What we would expect

```ts
export const relations = defineRelations(schema, (r) => ({
users: {
organization: r.one.organizations({
from: r.users.organizationId,
to: r.organizations.id,
}),
referrer: r.one.users({
from: r.users.referrerId,
to: r.users.id,
alias: 'users_referrerId_users_id',
}),
referred: r.many.users({ alias: 'users_referrerId_users_id' }),
},
organizations: {
users: r.many.users(),
},
}));
```

At minimum: never treat a table as a junction between X and itself, and always emit the `one` side of every foreign key.

#### Workaround

We generate `schema.ts` only and delete `relations.ts` on every pull, writing reads as explicit `leftJoin`s.

Contributor guide

Open the contributing guide

Research direction

Start with the relation generation path invoked by `drizzle-kit pull`, using the `drizzle.config.ts` entry point and comparing the generated `src/schema.ts` and `src/relations.ts` shown in the report. Reproduce the two-table MySQL schema, then verify that each foreign key produces its `one` relation and that a table is not selected as a junction between itself and another table.

Written by the indexing model from the issue text.

Assessment

Tech stack
mysql, node.js, typescript
Domain
databases, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.