drizzle-team / drizzle-team/drizzle-orm

[BUG]: `withReplicas` breaks `$with` selection, no `with recursive` support, missing array parsing for unbound `sql` fields

Open
#6,187 0 comments 0 reactions 0 assignees View on GitHub
bug
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.