drizzle-team / drizzle-team/drizzle-orm
[FEATURE]: Support relational projections in insert and update results
- 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
Drizzle's Relational Query Builder provides an excellent typed API for returning nested relational data through `columns` and `with`:
```ts
const post = await db.query.posts.findFirst({
where: { id: postId },
columns: {
id: true,
title: true,
},
with: {
author: {
columns: {
id: true,
name: true,
},
},
comments: {
columns: {
id: true,
body: true,
},
limit: 10,
orderBy: {
createdAt: 'desc',
},
},
},
})
```
However, `insert(...).returning()` and `update(...).returning()` can only return columns available to the mutation query. They cannot request the same nested relational result shape as `findFirst()` or `findMany()`.
A common API use case is to return a newly created or updated resource using the exact same representation as a read, including its relations.
#### Current workaround
The application must perform the mutation, capture the persisted identifier, and issue a second relational query manually:
```ts
const createdPost = await db.transaction(async (tx) => {
const [createdRow] = await tx
.insert(posts)
.values({
authorId,
title: 'Relational mutation result',
})
.returning({
id: posts.id,
})
if (createdRow === undefined) {
throw new Error('The insert returned no row')
}
return tx.query.posts.findFirst({
where: {
id: createdRow.id,
},
columns: {
id: true,
title: true,
},
with: {
author: {
columns: {
id: true,
name: true,
},
},
comments: {
columns: {
id: true,
body: true,
},
},
},
})
})
```
This introduces several responsibilities in userland:
- opening and managing a transaction;
- executing an additional query;
- duplicating the desired result projection;
- correlating returned identifiers with relational results;
- handling multi-row inserts and updates without creating an N+1 query pattern;
- restoring mutation-result ordering after a grouped relational read.
For updates, the original mutation predicate cannot safely be reused for the result query because the mutation may change one of the fields used by that predicate. The actual identifiers returned by the mutation must be used instead.
#### Proposed capability
Allow insert and update results to use the same relational projection contract as the Relational Query Builder.
The exact API name is only illustrative:
```ts
const [createdPost] = await db
.insert(posts)
.values({
authorId,
title: 'Relational mutation result',
})
.returningWith({
columns: {
id: true,
title: true,
},
with: {
author: {
columns: {
id: true,
name: true,
},
},
comments: {
columns: {
id: true,
body: true,
},
limit: 10,
orderBy: {
createdAt: 'desc',
},
},
},
})
```
This could alternatively be an overload of `returning()` or part of a future relational mutation API. The important part is reusing the RQB projection contract and its exact inferred result type.
Expected semantics:
- the projection represents the persisted state after the mutation;
- the mutation's actual returned primary keys identify the projected rows;
- root `columns` and nested `with` behave like their RQB equivalents;
- nested relations retain their exact inferred result types;
- the mutation and relational projection observe one consistent transaction;
- multi-row mutations can load all results through one grouped relational query rather than one query per row;
- mutations without relational projections retain the existing direct `returning()` path.
This request does not concern nested relational writes. It does not ask Drizzle to insert or update related records as part of the mutation input. Issue [[#4393](https://github.com/drizzle-team/drizzle-orm/issues/4393)](https://github.com/drizzle-team/drizzle-orm/issues/4393) covers that separate capability.
The implementation would not necessarily need to guarantee one SQL statement across every dialect. PostgreSQL could potentially use a data-modifying CTE or a transaction-bound relational read, while other dialects could expose the capability according to what they can support safely.
#### Real-world implementation experience
We encountered this while building a typed backend framework on top of Drizzle RQB v2.
Our current fallback:
1. executes the insert or update with `returning()` inside the mutation transaction;
2. retains the database-confirmed identifiers;
3. performs one relational projection inside the same transaction;
4. uses one grouped projection for collection mutations;
5. restores the mutation-result association after the grouped read.
This works, but the relational metadata and query-building capability already belong to Drizzle, so first-class support could provide a substantially cleaner and safer developer experience.
Is this capability planned for the Relational Query Builder or the Drizzle v1 roadmap? If not, would the team be open to a PostgreSQL-first proposal?
Contributor guide
Research direction
Start with the insert(...).returning() and update(...).returning() entry points, then compare them with the Relational Query Builder's findFirst() and findMany() projection examples. Define the API and dialect behavior for persisted relational projections, including multi-row ordering and transaction consistency, while preserving the existing direct returning() path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- postgresql, typescript
- Domain
- backend-api-design, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100