Rado GitHub

Joins

Combine tables with the usual join family, plus lateral variants for the databases that support them.

import {eq} from 'rado'

const rows = await db
  .select({title: Post.title, author: User.name})
  .from(Post)
  .innerJoin(User, eq(Post.authorId, User.id))

Available joins

db.select().from(A).innerJoin(B, on) // rows with a match in both
db.select().from(A).leftJoin(B, on) // all of A, B nullable
db.select().from(A).rightJoin(B, on) // all of B, A nullable
db.select().from(A).fullJoin(B, on) // all of both, either side nullable
db.select().from(A).crossJoin(B) // cartesian product, no condition

The join condition is any boolean expression, typically eq between two columns, but anything from Filter operators works, including and-ed combinations.

Nullability is tracked

Result types reflect the join semantics. After a leftJoin, the joined table's columns are nullable; after fullJoin, both sides are:

const rows = await db
  .select()
  .from(User)
  .leftJoin(Post, eq(Post.authorId, User.id))
// Array<{user: User, post: Post | null}>. Post is null when unmatched

With the default select(), joined results are grouped per table. Select explicit columns to flatten:

const flat = await db
  .select({name: User.name, title: Post.title})
  .from(User)
  .leftJoin(Post, eq(Post.authorId, User.id))
// Array<{name: string, title: string | null}>

Self-joins with alias

Joining a table to itself needs an alias to disambiguate:

import {alias, eq} from 'rado'

const Manager = alias(User, 'manager')

const rows = await db
  .select({employee: User.name, manager: Manager.name})
  .from(User)
  .leftJoin(Manager, eq(User.managerId, Manager.id))

Joining subqueries

Subqueries become joinable (and selectable) once they have a name via .as():

const postCounts = db
  .select({authorId: Post.authorId, posts: count()})
  .from(Post)
  .groupBy(Post.authorId)
  .as('postCounts')

const rows = await db
  .select({name: User.name, posts: postCounts.posts})
  .from(User)
  .leftJoin(postCounts, eq(postCounts.authorId, User.id))

More in Subqueries & CTEs.

Lateral joins (PostgreSQL/MySQL)

A lateral join lets the joined subquery reference columns from earlier tables. Think "for each row, run this subquery":

const latestPost = db
  .select()
  .from(Post)
  .where(eq(Post.authorId, User.id))
  .orderBy(desc(Post.createdAt))
  .limit(1)
  .as('latestPost')

const rows = await db
  .select({name: User.name, latest: latestPost.title})
  .from(User)
  .leftJoinLateral(latestPost, sql`true`)

Available variants: leftJoinLateral, innerJoinLateral, crossJoinLateral.

Tip: if you're reaching for a lateral join to fetch related rows, also look at Include. It expresses "fetch these related rows per result" directly and works on every dialect.