What version of drizzle-kit are you using?
1.0.0-rc.5
What version of drizzle-orm are you using?
1.0.0-rc.5
Describe the Bug
drizzle-kit pull treats a partial unique index (CREATE UNIQUE INDEX ... WHERE ...) as if it were a plain unique constraint when inferring relations, and generates a one-to-one relation where the data is one-to-many.
A partial unique index constrains a subset of rows. UNIQUE (user_id) WHERE archived_at IS NULL means "at most one active document per user" — a user may still have many documents in total. The generated relation says a user has exactly one.
This is silent: the generated code compiles, and queries traversing the relation return a single object instead of an array, so callers quietly see one row where they asked for all of them. It is a common pattern for soft-deleted or versioned rows, where "one current row per parent" is exactly the constraint you want.
Notably schema.ts does capture the predicate (.where(sql\(archived_at IS NULL)`)`), so the information is available at generation time and is only dropped by the relation inference.
Minimal reproduction
CREATE TABLE users (
id serial PRIMARY KEY
);
CREATE TABLE documents (
id serial PRIMARY KEY,
user_id integer NOT NULL REFERENCES users (id),
archived_at timestamptz
);
-- At most one ACTIVE document per user. A user may still have many documents.
CREATE UNIQUE INDEX documents_one_active_per_user
ON documents (user_id)
WHERE archived_at IS NULL;
The data this permits, which the generated relation contradicts:
INSERT INTO users (id) VALUES (1);
INSERT INTO documents (user_id, archived_at) VALUES (1, now()), (1, now()), (1, NULL);
-- 3 documents for user 1, all legal under the partial unique index
Then run drizzle-kit pull.
Actual output
relations.ts:
export const relations = defineRelations(schema, (r) => ({
documents: {
user: r.one.users({
from: r.documents.userId,
to: r.users.id
}),
},
users: {
documents: r.one.documents(), // <-- wrong
},
}))
schema.ts, which does retain the predicate:
export const documents = pgTable("documents", {
id: serial().primaryKey(),
userId: integer("user_id").notNull().references(() => users.id),
archivedAt: timestamp("archived_at", { withTimezone: true }),
}, (table) => [
uniqueIndex("documents_one_active_per_user").using("btree", table.userId.asc().nullsLast()).where(sql`(archived_at IS NULL)`),
]);
Expected output
users: {
documents: r.many.documents(),
},
Control
Dropping the predicate and leaving everything else identical produces the correct relation, which isolates the WHERE clause as the trigger:
DROP INDEX documents_one_active_per_user;
CREATE INDEX documents_user_id_idx ON documents (user_id);
users: {
documents: r.many.documents(), // correct
},
A full (non-partial) UNIQUE (user_id) correctly yields r.one, so only the partial case is affected.
Suggested fix
When deciding one-to-one vs one-to-many from a unique index, ignore any index whose indpred is non-null (pg_index.indpred IS NOT NULL), since such an index does not make the column unique across the table.
What version of
drizzle-kitare you using?1.0.0-rc.5
What version of
drizzle-ormare you using?1.0.0-rc.5
Describe the Bug
drizzle-kit pulltreats a partial unique index (CREATE UNIQUE INDEX ... WHERE ...) as if it were a plain unique constraint when inferring relations, and generates a one-to-one relation where the data is one-to-many.A partial unique index constrains a subset of rows.
UNIQUE (user_id) WHERE archived_at IS NULLmeans "at most one active document per user" — a user may still have many documents in total. The generated relation says a user has exactly one.This is silent: the generated code compiles, and queries traversing the relation return a single object instead of an array, so callers quietly see one row where they asked for all of them. It is a common pattern for soft-deleted or versioned rows, where "one current row per parent" is exactly the constraint you want.
Notably
schema.tsdoes capture the predicate (.where(sql\(archived_at IS NULL)`)`), so the information is available at generation time and is only dropped by the relation inference.Minimal reproduction
The data this permits, which the generated relation contradicts:
Then run
drizzle-kit pull.Actual output
relations.ts:schema.ts, which does retain the predicate:Expected output
Control
Dropping the predicate and leaving everything else identical produces the correct relation, which isolates the
WHEREclause as the trigger:A full (non-partial)
UNIQUE (user_id)correctly yieldsr.one, so only the partial case is affected.Suggested fix
When deciding one-to-one vs one-to-many from a unique index, ignore any index whose
indpredis non-null (pg_index.indpred IS NOT NULL), since such an index does not make the column unique across the table.