drizzle-team / drizzle-team/drizzle-orm
[BUG]: drizzle-kit push (pg): composite-PK column order introspected non-deterministically — PK dropped/recreated on random pushes
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
### What version of drizzle-orm are you using?
0.45.2
### What version of drizzle-kit are you using?
0.31.10 — and the root cause is still present in `drizzle-kit/src/serializer/pgSerializer.ts` on `main`
### Describe the Bug
`drizzle-kit push` against Postgres drops and re-adds a composite primary key on *some* pushes even when nothing in the schema changed — and **which** runs/databases are affected is non-deterministic (it can flip after unrelated DDL).
```ts
export const tags = pgTable('tag', { itemId: text('item_id').notNull(), tag: text().notNull() }, (t) => [
primaryKey({ name: 'tag_item_id_tag_pk', columns: [t.itemId, t.tag] }),
])
```
Repeated `drizzle-kit push --verbose` with zero schema changes emits on affected runs:
```sql
ALTER TABLE "tag" DROP CONSTRAINT "tag_item_id_tag_pk";
ALTER TABLE "tag" ADD CONSTRAINT "tag_item_id_tag_pk" PRIMARY KEY("item_id","tag");
```
We observed two databases with the identical schema disagree about whether the PK diffs, and the introspected column order flip between runs after DDL — declaring the "other" column order just moves which database churns.
### Root cause
`fromDatabase` builds composite-PK column order from this query, which has **no ORDER BY** — and `information_schema.constraint_column_usage` has no ordinal column at all, so row order is whatever the query plan happens to produce:
```sql
SELECT c.column_name, c.data_type, constraint_type, constraint_name, constraint_schema
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema
AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
WHERE tc.table_name = '${tableName}' and constraint_schema = '${tableSchema}';
```
The differ then compares that arbitrary order against the declared `columns` array as an ordered list → spurious, plan-dependent diffs.
### Suggested fix
Join `information_schema.key_column_usage` (whose `ordinal_position` is the position within the key) and order by it. We run this as a local `pnpm patch` and push is now fully idempotent across both our databases:
```sql
LEFT JOIN (
SELECT constraint_schema AS kcu_schema, constraint_name AS kcu_name,
column_name AS kcu_column, ordinal_position AS kcu_position
FROM information_schema.key_column_usage
) kcu ON kcu.kcu_schema = tc.constraint_schema
AND kcu.kcu_name = tc.constraint_name
AND kcu.kcu_column = ccu.column_name
WHERE tc.table_name = '${tableName}' and constraint_schema = '${tableSchema}'
ORDER BY kcu.kcu_position NULLS LAST, c.column_name;
```
(The rename-subquery avoids `constraint_schema` becoming ambiguous with the earlier `USING` join. Alternatively, compare PK columns as sets, or sort both sides before comparing.)
Related: #3338 reports the same recreate-every-push symptom on MySQL; #3103 covered order-only diffs at generate time.
Contributor guide
Research direction
Start in drizzle-kit/src/serializer/pgSerializer.ts and inspect the fromDatabase query that collects composite primary-key columns. Reproduce the issue with repeated drizzle-kit push --verbose runs against PostgreSQL, then verify that unchanged schemas no longer emit DROP/ADD primary-key statements and that column order remains stable across databases and runs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, sql, typescript
- Domain
- databases
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 82/100