drizzle-team / drizzle-team/drizzle-orm

[BUG]: pgTable extra-config array accepts arbitrary objects (empty `PrimaryKeyBuilder` type), and drizzle-kit silently drops them (declared indexes never reach the database)

Open
#6,140 2 comments 0 reactions 0 assignees View on GitHub
bug
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

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

1.0.0-rc.4

### Other packages

_No response_

### Describe the Bug

**What is the undesired behavior?**

Two compounding problems:

1. **Type level (drizzle-orm):** the `pgTable` extra-config callback is typed to return `PgTableExtraConfigValue[]`, but the published `.d.ts` for `PrimaryKeyBuilder` is structurally empty. Its only members are a constructor and a `static readonly [entityKind]` (the instance methods are `/** @internal */` and stripped from the published types). Since every non-null object is assignable to an empty object type (and excess-property checks don't fire against a propertyless union member), **any object whatsoever typechecks as a valid extra-config element**:

```ts
import { pgTable, uuid, uniqueIndex } from 'drizzle-orm/pg-core';

// Correct form: index IS generated.
export const good = pgTable('good', {
id: uuid('id').primaryKey(),
a: uuid('a').notNull(),
}, (t) => [
uniqueIndex('good_unique_a').on(t.a),
]);

// Builder wrapped in an object (a natural mistake when migrating off the
// deprecated object form): typechecks, but the index is silently dropped.
export const bad = pgTable('bad', {
id: uuid('id').primaryKey(),
a: uuid('a').notNull(),
}, (t) => [
{ myIndex: uniqueIndex('bad_unique_a').on(t.a) },
]);

// Not even builder-shaped but still typechecks.
export const worse = pgTable('worse', {
id: uuid('id').primaryKey(),
}, () => [
{ totallyBogus: 42, alsoBogus: 'hello' },
]);
```

`tsc --strict` accepts all three tables with zero errors. The minimal demonstration of the type hole:

```ts
import type { PrimaryKeyBuilder } from 'drizzle-orm/pg-core/primary-keys';
const pk: PrimaryKeyBuilder = { totallyBogus: 42 }; // compiles clean
```

2. **Runtime (drizzle-kit):** `generate` and `push` walk the extra-config array recognizing builders by entity kind and **silently skip everything else** No warning, no error. Output of `drizzle-kit generate` for the schema above:

```sql
CREATE TABLE "bad" (
"id" uuid PRIMARY KEY,
"a" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "good" (
"id" uuid PRIMARY KEY,
"a" uuid NOT NULL
);
--> statement-breakpoint
CREATE TABLE "worse" (
"id" uuid PRIMARY KEY
);
--> statement-breakpoint
CREATE UNIQUE INDEX "good_unique_a" ON "good" ("a");
```

`bad_unique_a` is gone, and `worse` was accepted without complaint.

This is particularly nasty because the schema, the migrations, and the database all agree with each other (the index is consistently absent everywhere) so no drift check can ever surface it. The `[{ key: builder }]` shape is exactly what you get by mechanically wrapping the old deprecated object form in an array, so codebases that migrated off the object syntax are likely to have phantom indexes they believe exist.

Real-world impact in our codebase: five indexes were declared this way and none existed in the database. One was a `uniqueIndex` guarding a business-key (duplicate rows had already crept in by the time we noticed), and another missing index turned a report query into a 38-second sequential-scan that exceeded our statement timeout (which is how we finally found this).

**What are the steps to reproduce it?**

1. Save the schema above as `schema.ts`, with this `drizzle.config.ts`:
```ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
dialect: 'postgresql',
schema: './schema.ts',
out: './migrations',
});
```
2. `tsc --noEmit --strict schema.ts` → exit 0, no diagnostics.
3. `drizzle-kit generate` → completes with `[✓]`, migration contains only `good_unique_a`.

**What is the desired result?**

Either (ideally both):

- **Close the type hole:** make the `PgTableExtraConfigValue` union members nominal (e.g. a `protected brand` on the builder classes, as `CheckBuilder` already has via `protected brand: 'PgConstraintBuilder'`), so plain objects are rejected at compile time. `PrimaryKeyBuilder` and `AnyIndexBuilder` are the permissive members today.
- **Fail loudly at runtime:** `drizzle-kit generate`/`push` (and `getTableConfig`) should warn or throw when an extra-config array element is not a recognized builder, instead of silently skipping it.

**Environment details:**

- TypeScript 5.9.3, `strict: true`, `skipLibCheck: true`. The schema file itself produces zero diagnostics regardless of `skipLibCheck`; without it, tsc reports unrelated pre-existing errors inside drizzle-orm's own published `.d.ts` (`cockroach-core/columns/*`, `Property 'config' does not exist`), so `skipLibCheck` is effectively required with 1.0.0-rc.4 anyway.
- Database: PostgreSQL (dialect `postgresql`); not driver-specific as the drop happens before any driver is involved
- Node.js 22, pnpm monorepo (also reproduced in a standalone single-file setup)

Contributor guide

Open the contributing guide

Research direction

Start with the published PrimaryKeyBuilder and AnyIndexBuilder types referenced from drizzle-orm/pg-core/primary-keys, then trace PgTableExtraConfigValue and getTableConfig. Reproduce the schema.ts example with tsc --noEmit --strict and drizzle-kit generate, then inspect how generate and push recognize extra-config builders. Done means invalid array elements are rejected or reported, and the declared bad_unique_a index reaches the generated migration.

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
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.