Drizzle returns nested posts from one findMany call

1 month 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 about 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
  1. 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.

    Slide text
    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
  2. 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.

    Slide text
    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
  3. 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.

    Slide text
    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
  4. 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.

    Slide text
    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
  5. 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.

    Slide text
    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
  6. 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.

    Slide text
    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
  7. 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.

    Slide text
    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
  8. 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.

    Slide text
    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
  9. 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.

    Slide text
    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
  10. 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.

    Slide text
    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
  11. 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.

    Slide text
    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
  12. 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.

    Slide text
    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
  13. 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.

    Slide text
    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
  14. 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.

    Slide text
    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
  15. 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.

    Slide text
    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
  16. 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?

    Slide text
    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

What happened in AI today, in your inbox

One short email each evening. Read it and you’re caught up.

Built in New York with ❤️