drizzle-team / drizzle-team/drizzle-orm
[FEATURE]: Expose generated constraint names as table.$constraints
- 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
Assessment
This issue has not been assessed yet.