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