drizzle-team / drizzle-team/drizzle-orm

[FEATURE]: Expose generated constraint names as table.$constraints

Open
#5,666 1 comment 2 reactions 0 assignees View on GitHub
enhancement
Dominant language
TypeScript
Stars
35.8k
Forks
1.6k
Avg merge
2d 7h
Merged PRs (30d)
4

Description

### Feature hasn't been suggested before.

- [x] I have verified this feature I'm about to request hasn't been suggested before.

### Describe the enhancement you want to request

## Summary

It would be really useful if every drizzle table carried a `$constraints`
object with the constraint names the schema produces, so user code can match
on DB constraint errors without hand-copying strings from the migration SQL.

## Problem

When an insert or update fails with a constraint violation — unique, FK,
check, etc. — Postgres tells you which constraint it was on the error object
(`error.cause.constraint`).
That's usually the cleanest way to decide whether to return a 409, a 400, or
something else.

The catch is that the exact constraint name isn't visible anywhere on the
TypeScript side. Drizzle knows it — it writes it into the generated migration
SQL — but from the app's point of view you have to open `drizzle/0000_xxx.sql`,
find the `CONSTRAINT` line, and hand-copy the string into your catch block.
And if you later rename a column, the generated name changes, but the string
you pasted still compiles fine and silently stops matching. You only notice
when production starts returning 500s instead of the 400 you expected.

## Proposal

Drizzle already computes every constraint's final name while building the
table (either up front, or inside the lazy `extraConfig` callback). The ask is
just to expose those names as a frozen object on the table itself:

```ts
table.$constraints = {
: "",
: "",
...
} as const
```

Key and value are both the full DB constraint name, exactly as it appears in
the migration SQL.

### Example — today

```ts
import { pgTable, serial, text, integer, unique, foreignKey } from "drizzle-orm/pg-core";
import { org } from "./org";

export const member = pgTable(
"member",
{
id: serial().primaryKey(),
orgId: integer("org_id").notNull(),
email: text().notNull(),
},
(t) => [
unique().on(t.email, t.orgId),
foreignKey({ columns: [t.orgId], foreignColumns: [org.id] }),
],
);
```

The constraints above get drizzle's auto-generated names:

- `member_email_org_id_unique`
- `member_org_id_org_id_fk`
- `member_pkey`

To handle insert errors, you'd open `drizzle/0000_init.sql`, read the names,
and hand-copy them:

```ts
async function createMember(input: { orgId: number; email: string }) {
try {
await db.insert(member).values(input);
} catch (error) {
if (error.cause?.constraint === "member_email_org_id_unique") {
throw new ConflictError("Email already in use in this organization");
}
if (error.cause?.constraint === "member_org_id_org_id_fk") {
throw new BadRequestError("Organization does not exist");
}
throw error;
}
}
```

Rename `orgId` to `organizationId` later and the FK name becomes
`member_organization_id_org_id_fk`. The old string still compiles, the
`if` never matches, and duplicate-org inserts start returning 500s.

### Example — with `table.$constraints`

```ts
async function createMember(input: { orgId: number; email: string }) {
try {
await db.insert(member).values(input);
} catch (error) {
if (error.cause?.constraint === member.$constraints.member_email_org_id_unique) {
throw new ConflictError("Email already in use in this organization");
}
if (error.cause?.constraint === member.$constraints.member_org_id_org_id_fk) {
throw new BadRequestError("Organization does not exist");
}
throw error;
}
}
```

A few things fall out of this:

- Typing `member.$constraints.` gives you an autocomplete list of exactly the
constraints that exist on the table.
- Rename a column and the generated key changes. The old key no longer exists
on `$constraints`, so every call site breaks at compile time and you have to
update them.
- The name lives on the same object you already imported for the query — no
need to go hunting through the migration folder.
- If you do pass `name: "..."` explicitly to `foreignKey` / `unique`, that
name becomes the key and the value. Same mechanism, nothing new to learn.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.