Skip to content

Deep Relations

UQL’s query syntax is context-aware. When you query a relation, the available fields and operators are automatically suggested and validated based on that related entity.

You can load a relation and select its specific fields using $populate.

You write
import { pool } from './uql.config.js';
import { User } from './shared/models/index.js';
const users = await pool.findMany(User, {
$select: { id: true, name: true },
$populate: {
profile: { $select: { picture: true } } // Load specific fields from a 1-1 relation
},
$where: {
email: { $iincludes: '@example.com' }
}
});
Generated SQL (PostgreSQL)
-- Main query with LEFT JOIN for OneToOne relation
SELECT "User"."id", "User"."name",
"profile"."id" "profile.id", -- the relation's primary key is always selected
"profile"."picture" "profile.picture" -- Prefixed alias for unflattening
FROM "User"
LEFT JOIN "Profile" "profile" ON "profile"."userId" = "User"."id" AND "profile"."deletedAt" IS NULL
WHERE "User"."email" ILIKE $1
-- values: ['%@example.com%']

Advanced: Deep Selection & Mandatory Relations

Section titled “Advanced: Deep Selection & Mandatory Relations”

Use $required: true inside a $populate block to enforce an INNER JOIN (by default UQL uses LEFT JOIN).

You write
import { User } from './shared/models/index.js';
const latestUsersWithProfiles = await pool.findOne(User, {
$select: { id: true, name: true },
$populate: {
profile: {
$select: { picture: true, bio: true },
$where: { bio: { $ne: null } },
$required: true // Enforce INNER JOIN
}
},
$sort: { createdAt: 'desc' },
});
Generated SQL (PostgreSQL)
-- INNER JOIN enforced by $required: true
SELECT "User"."id", "User"."name",
"profile"."id" "profile.id",
"profile"."picture" "profile.picture", "profile"."bio" "profile.bio"
FROM "User"
INNER JOIN "Profile" "profile" ON "profile"."userId" = "User"."id"
AND "profile"."bio" IS NOT NULL AND "profile"."deletedAt" IS NULL
ORDER BY "User"."createdAt" DESC
LIMIT 1

You can filter and sort when populating collections (One-to-Many or Many-to-Many).

You write
import { User } from './shared/models/index.js';
const authorsWithPopularPosts = await pool.findMany(User, {
$select: { id: true, name: true },
$populate: {
posts: {
$select: { title: true, createdAt: true },
$where: { title: { $iincludes: 'typescript' } },
$sort: { createdAt: 'desc' },
$limit: 5
}
},
$where: {
name: { $istartsWith: 'a' }
}
});
Generated SQL (PostgreSQL)
-- Main query (parent rows)
SELECT "User"."id", "User"."name" FROM "User"
WHERE "User"."name" ILIKE $1
Generated SQL (PostgreSQL, separate query)
-- OneToMany relation loaded via a second query
SELECT "Post"."title", "Post"."createdAt", "Post"."authorId"
FROM "Post"
WHERE "Post"."authorId" IN ($1, $2, ...)
AND "Post"."title" ILIKE $3
ORDER BY "Post"."createdAt" DESC
LIMIT 5
-- values: [...parentIds, '%typescript%']

UQL allows sorting by fields of related entities directly in the $sort object.

You write
import { Item } from './shared/models/index.js';
const items = await pool.findMany(Item, {
$select: { id: true, name: true },
$sort: {
tax: { name: 1 },
measureUnit: { name: 1 },
createdAt: 'desc'
}
});
Generated SQL (PostgreSQL)
SELECT "Item"."id", "Item"."name"
FROM "Item"
LEFT JOIN "Tax" "tax" ON "tax"."id" = "Item"."taxId" AND "tax"."deletedAt" IS NULL
LEFT JOIN "MeasureUnit" "measureUnit" ON "measureUnit"."id" = "Item"."measureUnitId" AND "measureUnit"."deletedAt" IS NULL
ORDER BY "tax"."name", "measureUnit"."name", "Item"."createdAt" DESC

 

Filter parent entities based on conditions on their ManyToMany or OneToMany relations. UQL compiles the condition to an EXISTS subquery, so it never joins in and duplicates parent rows.

The related entity’s own filters apply inside the subquery (the junction’s too, for ManyToMany), so a parent never matches through a row the query could not read - a trashed child, or one outside a security: true filter’s scope. On MongoDB the same conditions compile to correlated $lookup stages, so the query is unchanged across drivers.

You write
import { Post } from './shared/models/index.js';
// Find all posts that have a tag named 'typescript'
const posts = await pool.findMany(Post, {
$where: { tags: { name: 'typescript' } },
});
Generated SQL (PostgreSQL)
SELECT * FROM "Post"
WHERE EXISTS (
SELECT 1 FROM "PostTag"
WHERE "PostTag"."postId" = "Post"."id"
AND "PostTag"."tagId" IN (
SELECT "Tag"."id" FROM "Tag"
WHERE "Tag"."name" = $1 AND "Tag"."deletedAt" IS NULL
)
) AND "Post"."deletedAt" IS NULL
You write
// Find users who have authored posts with 'typescript' in the title
const users = await pool.findMany(User, {
$where: { posts: { title: { $iincludes: 'typescript' } } },
});
Generated SQL (PostgreSQL)
SELECT * FROM "User"
WHERE EXISTS (
SELECT 1 FROM "Post"
WHERE "Post"."authorId" = "User"."id"
AND "Post"."title" ILIKE $1 AND "Post"."deletedAt" IS NULL
) AND "User"."deletedAt" IS NULL
-- values: ['%typescript%']

Relation Count Filtering ($size Subqueries)

Section titled “Relation Count Filtering ($size Subqueries)”

Filter parent entities by the number of related records using $size on a relation key, type-checked against your entity’s relations and compiled to a COUNT(*) subquery. Accepts a number for exact match or any comparison operator ($eq, $ne, $gt, $gte, $lt, $lte, $between).

The count is scoped by the related entity’s filters, exactly like the EXISTS form above, so it never counts rows the same query could not read.

You write
// Find categories with at least 2 measure units
const categories = await pool.findMany(MeasureUnitCategory, {
$where: { measureUnits: { $size: { $gte: 2 } } },
});
Generated SQL (PostgreSQL)
SELECT * FROM "MeasureUnitCategory"
WHERE (SELECT COUNT(*) FROM "MeasureUnit"
WHERE "MeasureUnit"."categoryId" = "MeasureUnitCategory"."id"
AND "MeasureUnit"."deletedAt" IS NULL) >= $1
AND "deletedAt" IS NULL
You write
// Find items with more than 5 tags
const items = await pool.findMany(Item, {
$where: { tags: { $size: { $gt: 5 } } },
});
Generated SQL (PostgreSQL)
-- Tag has no filters of its own, so the junction count stands alone; when it does have
-- them, the counted junction rows narrow to the target ids that satisfy them.
SELECT * FROM "Item"
WHERE (SELECT COUNT(*) FROM "ItemTag"
WHERE "ItemTag"."itemId" = "Item"."id") > $1
You write
// Find items with between 2 and 10 tags
const items = await pool.findMany(Item, {
$where: { tags: { $size: { $between: [2, 10] } } },
});

  • Sub-Queries: Correlated sub-queries when the built-in ones are not enough.
  • Relation Mapping: How the relations you query here are declared.
  • Soft Delete: The filter that joins add to populated relations.
  • Streaming: Relation loading rules change when streaming.