Drizzle returns nested posts from one findMany call

1 hour ago

Drizzle ORM returns a user with an array of posts from one findMany call. That call is db.query.users.findMany, with posts true. Ada comes back with two posts already nested. The SQL Drizzle sent is one statement. drizzle-orm 0.45.2. Episode one of this series left a row in users. This sitting adds posts and reads both back in one call.

Ask

Ask about this presentation

Answers are generated from this presentation.

Chapters

  1. 0:00Nested posts from one findMany
  2. 0:24Two lines install Drizzle
  3. 0:480.45.2 still takes schema
  4. 1:19users table and posts table
  5. 1:36Relations go in under schema
  6. 2:00Ada and two nested posts
  7. 2:14A single SQL statement
  8. 2:290.45.2 toSQL is one left join lateral
  9. 3:07The relations graph is application-level
  10. 3:380.45.2 needs both sides of the graph
  11. 4:05schema plus drizzle plus findMany
  12. 4:27Nested JavaScript from one SQL statement
  13. 4:49db.query.users is undefined
  14. 5:23Pasting defineRelations fails to compile
  15. 6:05many posts throws until one users
  16. 6:28If you delete the relations objects
Show transcript

Nested posts from one findMany

Drizzle ORM returns nested posts from one findMany call
drizzle-orm 0.45.2 · drizzle-kit 0.31.10
Ada, two posts already nested
index.ts
const result = await db.query.users.findMany({
  with: {
    posts: true,
  },
});
bun index.ts
$ bun index.ts
[
  {
    id: 1,
    name: "Ada",
    email: "ada@example.com",
    createdAt: 2026-09-11T11:18:33.188Z,
    posts: [
      {
        id: 1,
        title: "SQL is awesome",
        content: "first post",
        authorId: 1,
      }, {
        id: 2,
        title: "But check relational queries",
        content: "second post",
        authorId: 1,
      }
    ],
  }
]
0.45.2 toSQL, one statement
select "users"."id", "users"."name",
  "users"."email", "users"."created_at",
  "users_posts"."data" as "posts"
from "users" "users"
left join lateral (
  select coalesce(
    json_agg(json_build_array(
      "users_posts"."id", "users_posts"."title",
      "users_posts"."content", "users_posts"."author_id"
    )), '[]'::json) as "data"
  from "posts" "users_posts"
  where "users_posts"."author_id" = "users"."id"
) "users_posts" on true
Source: assets/lab/index.ts, assets/captures/05-nested-result.txt, assets/data/tosql-findmany.sql, run 2026-09-11

Drizzle ORM returns a user with an array of posts from one findMany call. That call is db.query.users.findMany, with posts true. Ada comes back with two posts already nested. The SQL Drizzle sent is one statement. drizzle-orm 0.45.2. Episode one of this series left a row in users. This sitting adds posts and reads both back in one call.

Two lines install Drizzle

01
Install
Two lines install drizzle-orm 0.45.2 and drizzle-kit 0.31.10
terminal
$ bun add drizzle-orm@0.45.2 pg
bun add v1.2.23 (cf136713)
Resolving dependencies
Resolved, downloaded and extracted [41]
Saved lockfile

installed drizzle-orm@0.45.2
installed pg@8.23.0

38 packages installed [3.67s]

$ bun add -d drizzle-kit@0.31.10 @types/pg
bun add v1.2.23 (cf136713)
Saved lockfile

installed drizzle-kit@0.31.10 with binaries:
 - drizzle-kit
installed @types/pg@8.23.1

[22.00ms] done
drizzle-orm 0.45.2 · pg 8.23.0
runtime
drizzle-kit 0.31.10 · @types/pg
dev only
Source: assets/captures/01-install.txt, run 2026-09-11 on Bun 1.2.23

Install is two lines. bun add drizzle-orm at 0.45.2 and pg puts the query builder and the PostgreSQL driver in the project. bun add dash d drizzle-kit at 0.31.10 adds the command line tool as a development dependency. The first command installed thirty eight packages in three and seven tenths of a second.

0.45.2 still takes schema

0.45.2 still takes schema, and the live docs pass relations
drizzle-orm 0.45.2
drizzle-kit 0.31.10
PostgreSQL 17.10
Relational Queries v1
live Query and Relations pages install drizzle-orm@rc and show defineRelations
0.45.2 wants tables and relations() objects under the name schema
Source: registry.npmjs.org/drizzle-orm, registry.npmjs.org/drizzle-kit, read 2026-09-11 · orm.drizzle.team/docs/relations, captured in assets/docs/relations-opening-rc.png

drizzle-orm 0.45.2 and drizzle-kit 0.31.10, on PostgreSQL. This is the stable line. A version one is on the way, 1.0.0 release candidate 4. The live Query page currently installs drizzle-orm at rc, and shows defineRelations. 0.45.2 still takes the tables and the relations objects under the name schema. The live docs pass a v2 object under the name relations. Mix the keys. db.query comes back empty.

users table and posts table

02
First run
schema.ts now has a users table and a posts table
authorId points at the user
schema.ts
export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content").notNull(),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id),
});
Source: assets/lab/schema.ts

The smallest program starts here. It returns nested rows. schema dot ts now has two tables. users is already in the series, with id, name, a unique email, and a created-at stamp. posts adds a title, content, and an authorId column pointing at the user.

Relations go in under schema

Both relations exports go into drizzle under the name schema
schema.ts
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";

const db = drizzle(process.env.DATABASE_URL!, { schema });
usersRelations · many posts
postsRelations · one author
Source: assets/lab/schema.ts, assets/lab/index.ts

Two relations exports. usersRelations says users have many posts. postsRelations says a post has one author, matching authorId to users.id. Pass the tables and those relation objects into drizzle under the name schema. db.query is compiled from that schema object. db.select from users still works if you skip the schema object. db.query.users needs the schema object.

Ada and two nested posts

findMany with posts true printed Ada and two nested posts
bun index.ts
$ bun index.ts
[
  {
    id: 1,
    name: "Ada",
    email: "ada@example.com",
    createdAt: 2026-09-11T11:18:33.188Z,
    posts: [
      {
        id: 1,
        title: "SQL is awesome",
        content: "first post",
        authorId: 1,
      }, {
        id: 2,
        title: "But check relational queries",
        content: "second post",
        authorId: 1,
      }
    ],
  }
]
users · 1
posts · 2
Source: assets/captures/05-nested-result.txt, run 2026-09-11

bun index dot ts prints one user, Ada. posts is an array of two objects. Title one, SQL is awesome. Title two, But check relational queries. The posts array is already grouped on the way out.

A single SQL statement

03
The shape
The Query page says a single SQL statement is outputted by Drizzle
Source: orm.drizzle.team/docs/rqb, captured 2026-09-11 · assets/docs/rqb-single-statement.png

On the Query page, under Partial fields select, the manual says keep in mind that a single SQL statement is outputted by Drizzle. findMany with posts true can return a nested object. Drizzle compiles one statement.

0.45.2 toSQL is one left join lateral

0.45.2 toSQL is one left join lateral plus json_agg
0.45.2 toSQL
select "users"."id", "users"."name",
  "users"."email", "users"."created_at",
  "users_posts"."data" as "posts"
from "users" "users"
left join lateral (
  select coalesce(
    json_agg(json_build_array(
      "users_posts"."id",
      "users_posts"."title",
      "users_posts"."content",
      "users_posts"."author_id"
    )),
    '[]'::json
  ) as "data"
  from "posts" "users_posts"
  where "users_posts"."author_id" = "users"."id"
) "users_posts"
on true
statements · 1
our capture uses json_build_array and aliases users / users_posts
live docs, v2 generator
Select with joins tab
Source: assets/data/tosql-findmany.sql, assets/captures/06-tosql.txt · live block: orm.drizzle.team/docs/rqb captured in assets/docs/rqb-include-subqueries.png · joins tab: assets/docs/relations-leftjoin-tab.png

toSQL on 0.45.2 prints one select, a left join lateral, and json_agg of json_build_array. LATERAL means Postgres runs the inner select once per user row. json_agg packs those related rows into one JSON array in the same SELECT. The driver never groups a flat join list. The live Include-subqueries block is the v2 generator. It uses row_to_json and aliases d0 and d1. Our capture uses json_build_array. The capture is the proof. The Relations page Select with joins tab is leftJoin plus a mappedResult you write yourself. Relational queries skip that mapping.

The relations graph is application-level

The relations graph is application-level, and db.query reads it
two schemas, with foreign keys and without, work the same for relational queries
Source: orm.drizzle.team/docs/relations, captured 2026-09-11 · assets/docs/relations-foreign-keys.png

On the Relations page, the Foreign keys section. Relations do not affect the database schema in any way and do not create foreign keys implicitly. Two schemas, one with foreign keys and one without, will work exactly the same for relational queries. The relations graph is a TypeScript object that says which tables point at which. db.query reads that object. Our lab has the foreign key and the relations graph. db.query reads the graph. The references call on authorId is for migrations.

0.45.2 needs both sides of the graph

0.45.2 throws until both sides of the graph exist
v2 can declare many alone
gotcha-many-only.ts
$ bun gotcha-many-only.ts
query keys [ "users", "posts" ]
THREW Error There is not enough
information to infer relation
"users.posts"
Source: orm.drizzle.team/docs/relations-v1-v2, captured in assets/docs/v1v2-many-without-one.png · assets/captures/13-many-only.txt

usersRelations says many posts. findMany with posts true still throws until postsRelations declares one users, with fields and references. The error reads There is not enough information to infer relation users.posts. The v1 to v2 page names this Define many without one. The v2 line can declare many alone. 0.45.2 still needs both sides of the graph. The matching one is what lets with posts true compile a join.

schema plus drizzle plus findMany

04
The real thing
schema plus drizzle schema plus findMany is the whole path
schema.ts
import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content").notNull(),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import * as schema from "./schema";

const db = drizzle(process.env.DATABASE_URL!, { schema });

const result = await db.query.users.findMany({
  with: {
    posts: true,
  },
});
Source: assets/lab/schema.ts, assets/lab/index.ts

schema exports users, posts, usersRelations, and postsRelations. index imports the whole file as schema. drizzle gets the connection string and that schema object. Then db.query.users.findMany, with posts true. with posts true is how you ask for the nested array. The Query page columns option picks which fields come back. It still emits one statement.

Nested JavaScript from one SQL statement

Nested JavaScript comes back from one SQL statement
bun index.ts
$ bun index.ts
[
  {
    id: 1,
    name: "Ada",
    email: "ada@example.com",
    createdAt: 2026-09-11T11:18:33.188Z,
    posts: [
      { id: 1, title: "SQL is awesome",
        content: "first post", authorId: 1 },
      { id: 2, title: "But check relational queries",
        content: "second post", authorId: 1 }
    ],
  }
]
bun tosql.ts
select "users"."id", "users"."name",
  "users"."email", "users"."created_at",
  "users_posts"."data" as "posts"
from "users" "users"
left join lateral (
  select coalesce(
    json_agg(json_build_array(
      "users_posts"."id", "users_posts"."title",
      "users_posts"."content", "users_posts"."author_id"
    )),
    '[]'::json) as "data"
  from "posts" "users_posts"
  where "users_posts"."author_id" = "users"."id"
) "users_posts" on true
1
declare the graph
2
pass it as schema on 0.45.2
3
call db.query
Source: assets/captures/05-nested-result.txt, assets/data/tosql-findmany.sql

bun index prints Ada, posts already nested. Two titles: SQL is awesome, and But check relational queries. bun tosql prints one select, left join lateral, json_agg of json_build_array. Declare the graph. Pass it as schema on 0.45.2. Call db.query. Nested JavaScript comes back from one SQL statement.

db.query.users is undefined

05
Gotchas
db.query.users is undefined when drizzle never received the graph
drizzle-orm #4390 · closed 2026-01-03 · labelled rqb
drizzle-orm issue 4390 Closed
empty-query.ts
$ bun empty-query.ts
query keys []
query.users undefined
typeof query.users undefined
select().from(users) rows 1
query.users.findMany THREW TypeError
undefined is not an object
(evaluating 'db.query.users.findMany')
same hole with the live-docs key relations
$ bun gotcha-relations-key.ts
query keys with { relations } []
query.users undefined
tables only in schema · query.users exists · with posts throws relation.referencedTable
Source: github.com/drizzle-team/drizzle-orm/issues/4390 · assets/captures/07-empty-query.txt, 12-relations-key.txt, 11-schema-only.txt

This is the part people get wrong. db.select from users works. db.query.users is undefined. That is drizzle-orm issue 4390, closed January 2026, labelled rqb. drizzle never received the graph. On 0.45.2, omit schema. query keys prints an empty array. Pass the live-docs key relations. You get the same empty query. Tables only in schema is quieter. query.users exists. with posts true throws relation.referencedTable. On the rc line the empty-query miss is omitting relations.

Pasting defineRelations fails to compile

Pasting defineRelations onto 0.45.2 fails to compile
package.json
{
  "dependencies": {
    "drizzle-orm": "0.45.2",
    "pg": "^8.23.0"
  }
}
bun paste-v2.ts
$ bun paste-v2.ts
SyntaxError: Export named
'defineRelations' not found
in module '.../drizzle-orm/index.js'.
Bun v1.2.23 (macOS arm64)
tsc
error TS2305: Module "drizzle-orm"
has no exported member
'defineRelations'.
'relations' does not exist in type
'DrizzleConfig'
option rename, schema to relations
Source: orm.drizzle.team/docs/relations, orm.drizzle.team/docs/relations-v1-v2 · assets/captures/09-paste-v2-bun.txt, 10-paste-v2-tsc.txt, 08-exports.txt

This is the part people get wrong. The Query and Relations pages install drizzle-orm at rc and show defineRelations. The docs skip this. npm i drizzle-orm still gives 0.45.2. Paste the live snippet onto 0.45.2 and Bun throws SyntaxError: Export named defineRelations not found. tsc says TS2305, no exported member defineRelations. tsc also says relations does not exist in type DrizzleConfig. The v1 to v2 page renames the option from schema to relations. Mix the names. db.query is empty. The v1 form that compiles is relations per table, passed as schema.

many posts throws until one users

many posts throws until postsRelations declares one users
gotcha-many-only.ts
$ bun gotcha-many-only.ts
query keys [ "users", "posts" ]
THREW Error There is not enough
information to infer relation
"users.posts"
Source: orm.drizzle.team/docs/relations-v1-v2 · assets/captures/13-many-only.txt

This is the part people get wrong. On 0.45.2, usersRelations with only many posts throws There is not enough information to infer relation users.posts. The matching postsRelations, one users with fields and references, is what lets with posts true compile a join. The v1 to v2 page calls this Define many without one.

If you delete the relations objects

If you delete the relations objects, what does db.query still know?
schema.ts
import { integer, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  createdAt: timestamp("created_at").defaultNow().notNull(),
});

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  content: text("content").notNull(),
  authorId: integer("author_id")
    .notNull()
    .references(() => users.id),
});

export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));
index.ts
const db = drizzle(process.env.DATABASE_URL!, { schema });

const result = await db.query.users.findMany({
  with: { posts: true },
});
drizzle-orm 0.45.2
drizzle-kit 0.31.10
PostgreSQL 17.10
drizzle-orm #4390
Source: assets/lab/schema.ts, assets/lab/index.ts · registry.npmjs.org/drizzle-orm, registry.npmjs.org/drizzle-kit, read 2026-09-11

The whole path is two tables, two relations exports, schema into drizzle, and one findMany with posts true. A nested object comes back. One SQL statement went out. drizzle-orm 0.45.2, drizzle-kit 0.31.10. If you delete the relations objects and keep the foreign keys, what does db.query still know?