drizzle-team / drizzle-team/drizzle-orm
[BUG]: PostgreSQL push re-diffs its generated composite FK, expression indexes and empty text[] defaults
- Dominant language
- TypeScript
- Stars
- 35.8k
- Forks
- 1.6k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 4
Description
## Environment
- drizzle-kit 0.31.10 (current npm latest when reproduced)
- drizzle-orm 0.44.x / postgres-js
- PostgreSQL 16 (postgres:16-alpine, isolated local database)
## Reproduction
Define a parent with id text PK, project_id text, UNIQUE(id, project_id), and a child with project_id text before thread_id text in column order. Define:
```ts
foreignKey({ columns: [child.threadId, child.projectId], foreignColumns: [parent.id, parent.projectId], name: "child_thread_project_fk" })
uniqueIndex("child_live_actor_idx").on(child.projectId, child.actorId, child.scope, sql`coalesce(${child.threadId}, '')`).where(sql`${child.revokedAt} IS NULL AND ${child.memberType} = 'agent'`)
text("tags").array().notNull().default([])
```
Generate/apply the schema using Drizzle itself, then run `drizzle-kit push --verbose` again without any schema changes. The next plan still drops/adds the composite FK, drops/creates the expression index and SET DEFAULT on the empty text array. Applying that plan and repeating produces the same churn. Using a raw SQL default containing an explicit PostgreSQL text-array cast also does not remove array churn.
## Observed introspection
`drizzle-kit pull` reconstructs the composite FK source columns as [projectId, threadId] but foreign columns as [id, projectId], losing their positional pairing. In the installed introspection SQL, pg_attribute joins use attnum=ANY(conkey) and fatt.attnum=ANY(confkey) independently, giving a Cartesian product rather than pairing conkey/confkey by ordinality.
The expression index is reconstructed with every index column as an SQL expression (including ordinary project_id/actor_id/scope), and normalized expression/predicate casts and parentheses. Empty text[] DEFAULT '{}'::text[] is reconstructed as `.default([""])`, not `.default([])`.
Expected: a second push of Drizzle-generated/applied SQL emits no DDL, preserves composite column pairing and keeps empty-array semantics. We have retained the correct FK/index/empty-array declarations and currently test an exact allowlist of the repeated benign DDL rather than changing invariants to match introspection.
## Exported pushSchema API observation
The CLI can inspect this schema, but exported pushSchema fails when it reaches a composite PK: its query wrapper ignores the params argument and calls execute(sql.raw(query)), so the primary-key introspection query containing $1/$2 reaches PostgreSQL with no parameters (`there is no parameter $1`). This is separate from the above round-trip issues; CLI reproduction does not require using that API.
No production data is needed; reproduction uses an empty disposable DB. Happy to split these related introspection findings into separate issues if preferred.
Contributor guide
Research direction
Start with the drizzle-kit push --verbose and drizzle-kit pull reproduction, then inspect the installed introspection SQL, especially the pg_attribute joins for composite foreign keys and indexes. Also trace the exported pushSchema query wrapper mentioned in the report. Done means a second push emits no DDL, composite columns retain their pairing, empty text[] defaults remain empty, and pushSchema passes query parameters.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, typescript
- Domain
- databases, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100