zanith

Subqueries

EXISTS, IN (subquery), CTEs, and FROM subquery — for composing complex queries from simpler building blocks.

When to use subqueries

Subqueries let you use the result of one query inside another. This is essential for questions like "find users who have at least one published post" or "show me only data from the last 7 days." Zanith supports four subquery patterns.

Outer query
WHERE
Subquery

inner query

Result

EXISTS

The most common subquery pattern. "Find records where a related subquery returns at least one result." More efficient than JOIN when you only need to check existence.

TSusers with published posts
import { exists, notExists } from 'zanith';
 
// Users who have at least one published post
const activeAuthors = await db.user.query()
.where(({ user }) =>
exists(
db.post.query()
.where(({ post }) => post.published.eq(true))
)
)
.execute();

NOT EXISTS

TS
// Users who have never posted anything
const lurkers = await db.user.query()
.where(({ user }) =>
notExists(
db.post.query()
)
)
.execute();

IN (subquery)

Check if a field's value appears in the results of another query:

TS
import { inSubquery, notInSubquery } from 'zanith';
 
// Users whose IDs appear in the post authors list
const result = await db.user.query()
.where(({ user }) =>
inSubquery(
user.id,
db.post.query()
.select(({ post }) => ({ authorId: post.authorId }))
)
)
.execute();

CTE (WITH clause)

Common Table Expressions let you define a temporary named result set, then query from it. This makes complex queries more readable by breaking them into logical steps.

TSrecent posts CTE
const results = await db.post.query()
// Step 1: Define the CTE
.withCTE(
'recent',
"SELECT * FROM posts WHERE created_at > NOW() - INTERVAL '7 days'"
)
// Step 2: Query FROM the CTE instead of the raw table
.fromCTE('recent')
// Step 3: Add additional filters
.where(({ post }) => post.published.eq(true))
.limit(20)
.execute();
 
// Generated SQL:
// WITH "recent" AS (
// SELECT * FROM posts WHERE created_at > NOW() - INTERVAL '7 days'
// )
// SELECT "posts".*
// FROM "recent" AS "posts"
// WHERE "posts"."published" = TRUE
// LIMIT $1

FROM subquery

Use a raw SQL subquery as the FROM source. Useful when you need a pre-filtered or pre-transformed dataset as the starting point.

TS
const results = await db.post.query()
.fromSubquery(
'SELECT * FROM posts WHERE published = true',
[], // params (none in this case)
'pub' // alias for the subquery
)
.where(({ post }) => post.title.contains('guide'))
.execute();
 
// Generated SQL:
// SELECT "pub".*
// FROM (SELECT * FROM posts WHERE published = true) AS "pub"
// WHERE "pub"."title" ILIKE $1

Function reference

FunctionSQL patternImport from
exists(subquery)WHERE EXISTS (SELECT ...)'zanith'
notExists(subquery)WHERE NOT EXISTS (SELECT ...)'zanith'
inSubquery(field, subquery)WHERE field IN (SELECT ...)'zanith'
notInSubquery(field, subquery)WHERE field NOT IN (SELECT ...)'zanith'
.withCTE(name, sql)WITH name AS (sql)Query builder method
.fromCTE(name)FROM name AS aliasQuery builder method
.fromSubquery(sql, params, alias)FROM (sql) AS aliasQuery builder method