Select
Everything about reading data: shaping the selection, filtering, ordering, grouping, pagination and a few power tools.
import {eq} from 'rado'
const users = await db.select().from(User).where(eq(User.name, 'Ada'))
What to select
Everything
const users = await db.select().from(User)
// Array<{id: number, name: string, email: string | null}>
With joins involved, select() returns the columns of all joined tables (see
Joins for how they're combined).
An object of columns and expressions
const rows = await db
.select({
id: User.id,
name: User.name,
nameLength: sql`length(${User.name})`
})
.from(User)
// Array<{id: number, name: string, nameLength: number}>
Selections can be nested. The result mirrors the shape:
const rows = await db
.select({
id: User.id,
contact: {name: User.name, email: User.email}
})
.from(User)
// Array<{id: number, contact: {name: string, email: string | null}}>
Spreading a table is useful for "everything plus":
const rows = await db
.select({...User, postCount: count(Post.id)})
.from(User)
.leftJoin(Post, eq(Post.authorId, User.id))
.groupBy(User.id)
A single expression
Select a bare field or expression and you get an array of values instead of objects:
const names = await db.select(User.name).from(User)
// Array<string>
const [total] = await db.select(count()).from(User)
// number
Renaming with .as()
Any expression can be aliased:
const rows = await db
.select({upper: sql`upper(${User.name})`.as('upper_name')})
.from(User)
Filtering with where
where accepts one or more conditions. Multiple arguments are combined with
and:
import {and, eq, gt, isNull, or} from 'rado'
await db.select().from(User).where(eq(User.name, 'Ada'))
// These two are equivalent:
await db.select().from(User).where(gt(User.id, 10), isNull(User.email))
await db
.select()
.from(User)
.where(and(gt(User.id, 10), isNull(User.email)))
// Compose freely
await db
.select()
.from(User)
.where(or(eq(User.name, 'Ada'), and(gt(User.id, 10), isNull(User.email))))
The full operator catalog lives in Filter operators.
Conditions passed as undefined are ignored, which makes optional filters
easy to assemble:
function findUsers(filters: {name?: string; minId?: number}) {
return db
.select()
.from(User)
.where(
filters.name ? eq(User.name, filters.name) : undefined,
filters.minId ? gt(User.id, filters.minId) : undefined
)
}
Ordering
import {asc, desc} from 'rado'
await db.select().from(User).orderBy(asc(User.name))
await db.select().from(Post).orderBy(desc(Post.createdAt), asc(Post.title))
Grouping and having
import {count, gt} from 'rado'
const prolific = await db
.select({authorId: Post.authorId, posts: count()})
.from(Post)
.groupBy(Post.authorId)
.having(gt(count(), 5))
More aggregate functions in Aggregates.
Pagination
const pageSize = 10
const page = 3
await db
.select()
.from(Post)
.orderBy(desc(Post.createdAt))
.limit(pageSize)
.offset((page - 1) * pageSize)
Distinct
// Distinct rows
await db.selectDistinct({name: User.name}).from(User)
// PostgreSQL: distinct on specific columns
await db
.selectDistinctOn([Post.authorId], {
authorId: Post.authorId,
title: Post.title
})
.from(Post)
.orderBy(Post.authorId, desc(Post.createdAt))
Getting one row (or one value)
Queries are thenable and iterate to an array by default. Two execution helpers change that:
const all = await db.select().from(User) // Array<Row>
const allToo = await db.select().from(User).all() // same
const first = await db.select().from(User).get() // Row | undefined
On synchronous drivers (better-sqlite3, bun:sqlite, sql.js) both .all()
and .get() return results directly without awaiting.
Counting rows
db.$count is a shortcut for the count-with-optional-filter dance:
const total = await db.$count(User)
const filtered = await db.$count(User, gt(User.id, 10))
Row locking (PostgreSQL/MySQL)
await db.select().from(User).where(eq(User.id, 1)).for('update') // select ... for update
await db.select().from(User).for('share', {of: User, skipLocked: true})
Immutability: build once, branch freely
Every method returns a new query. A base query can safely fan out:
const posts = db.select().from(Post)
const drafts = posts.where(eq(Post.published, false))
const recent = posts.orderBy(desc(Post.createdAt)).limit(5)
// `posts` is still unfiltered; all three are independent queries
This also means partial queries are perfectly good values to pass around, store in constants or export from modules.
Dynamic query building
Because of immutability, conditional chaining requires reassignment. Annotate
the query with $dynamic() to keep the types happy while reassigning:
let query = db.select().from(Post).$dynamic()
if (onlyPublished) query = query.where(eq(Post.published, true))
if (sorted) query = query.orderBy(desc(Post.createdAt))
const results = await query
Inspecting the SQL
Curious what a query compiles to? Every query exposes toSQL:
const {sql, params} = db.select().from(User).where(eq(User.id, 1)).toSQL()
// sql: 'select "user"."id", ... from "user" where "user"."id" = $1'
See Prepared statements for more.