drizzle-team / drizzle-team/drizzle-orm
[BUG]: `withReplicas` breaks `$with` selection, no `with recursive` support, missing array parsing for unbound `sql` fields
- 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
**Environment**
- drizzle-orm: `1.0.0-rc.4`
- driver: `drizzle-orm/bun-sql` (Bun native SQL), PostgreSQL
- runtime: Bun, macOS
---
## 1. Bug: `withReplicas` drops the `selection` argument of `$with`
`withReplicas` forwards only the first argument:
```js
// node_modules/drizzle-orm/pg-core/async/db.js
const $with = (arg) => getReplica(replicas).$with(arg);
```
So the documented "CTE from raw SQL with declared selection" pattern silently loses its selection and every field access on the CTE returns `undefined`:
```ts
import { sql } from "drizzle-orm";
import { withReplicas } from "drizzle-orm/pg-core";
const db = withReplicas(primary, [replica]);
const cte = db
.$with("cte", {
value: sql``.as("value"),
})
.as(sql`select 1 as "value"`);
// cte.value is undefined here
await db.with(cte).select({ value: cte.value }).from(cte);
```
Result at execution time:
```
TypeError: Object.entries requires that input parameter not be null or undefined
at orderSelectedFields (drizzle-orm/utils.js:192)
at getSQL (drizzle-orm/pg-core/query-builders/select.js:679)
```
The same query works when the CTE is created via `new QueryBuilder().$with(...)` or directly on the primary db instance.
**Expected:** `const $with = (...args) => getReplica(replicas).$with(...args);` (applies to all dialects' `withReplicas`).
---
## 2. Feature request: `with recursive` support (`$withRecursive`)
`buildWithCTE` always emits `with "alias" as (...)` — there is no way to get the `recursive` keyword. A hierarchy query currently requires nesting the recursive CTE inside a derived table as raw SQL:
```ts
const tree = new QueryBuilder()
.$with("tree", {
id: node.id,
path: sql``.as("path"),
})
.as(sql`
select * from (
with recursive "t" as (
select ${node.id} as "id", array[${node.id}] as "path"
from ${node} where ${isNull(node.parentId)}
union all
select ${node.id}, t."path" || ${node.id}
from ${node} join "t" t on ${node.parentId} = t."id"
)
select * from "t"
) _t
`);
```
Would be great to have a first-class API, e.g.:
```ts
db.$withRecursive("tree").as((qb) => baseQuery.unionAll(recursiveQuery));
```
---
## 3. Missing array parsing for `sql` fields not bound to a column
Fields declared as raw `sql` (no table column attached — e.g. fields of a raw-SQL CTE, or computed expressions) have no decoder, so with the bun-sql driver a `uuid[]` value arrives as raw postgres text instead of an array:
```ts
const rows = await db.with(tree).select({ path: tree.path }).from(tree);
// rows[0].path === "{0cdaa5a9-...,1a628ebb-...}" (string, not string[])
// even though the field is typed sql
```
Related: in set operations (`unionAll`), result mapping takes decoders **only from the left select's fields**. So even a real column on the right side (e.g. a `timestamp`) comes back unparsed when the left side uses a raw `sql` placeholder:
```ts
const a = db.select({ ts: sql`null`.as("ts") }).from(x);
const b = db.select({ ts: y.createdAt }).from(y); // timestamp column
await a.unionAll(b); // ts from `b` rows is a raw string
```
Current workarounds:
```ts
path: sql``.mapWith(parsePgArray).as("path"),
ts: sql`null`.mapWith(y.createdAt).as("ts"),
```
**Expected:** either type-driven parsing for `sql` fields, or at least applying the right select's decoders positionally in set operations, so column-bound fields keep their parsing.
Contributor guide
Research direction
Start with the withReplicas implementation in pg-core/async/db.js, then trace $with, buildWithCTE, and set-operation result mapping. Treat the three reported behaviors as separate work items; done means selection arguments are preserved, recursive CTEs are supported as requested, and unbound or positional fields receive the expected parsing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, postgresql, typescript
- Domain
- backend-api-design, databases
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100