Skip to content
NewFrom Mongoose to SQL: migrating MongoDB to PostgreSQL7 min read

Sorting

$sort orders rows by fields of the entity, JSON paths inside them, or a joined relation’s fields, in the order you write the keys:

const posts = await pool.findMany(Post, {
$select: { title: true, publishedAt: true },
$sort: { publishedAt: 'desc', title: 'asc' },
});
PostgreSQL
ORDER BY "publishedAt" DESC, "title"

A direction is 'asc' or 1, 'desc' or -1, or a null placement.

Engines disagree on where nulls go, and a plain direction keeps each engine’s answer:

Engine Plain asc puts nulls
PostgreSQL, Neon, PGlite, Bun SQL on Postgres last
CockroachDB, SQLite, libSQL, Turso, D1, MySQL, MariaDB, SQL Server first
MongoDB first (a missing field counts as null)

Name a placement and the order is the same everywhere:

const posts = await pool.findMany(Post, {
$sort: { publishedAt: 'descNullsLast' },
});

The placements are ascNullsFirst, ascNullsLast, descNullsFirst and descNullsLast. They work wherever a direction does, including a populated relation’s $sort and an aggregate’s. Engines without NULLS LAST get an equivalent:

PostgreSQL, CockroachDB, SQLite, libSQL, Turso, D1
ORDER BY "publishedAt" DESC NULLS LAST
MySQL, MariaDB
ORDER BY `publishedAt` IS NULL, `publishedAt` DESC
SQL Server
ORDER BY CASE WHEN "publishedAt" IS NULL THEN 1 ELSE 0 END, "publishedAt" DESC

On MongoDB, UQL sorts by a null flag first and strips it from the results.

Where the entity is a type parameter, a key held in one cannot build the map: TypeScript widens { [key]: 'asc' } to an index signature, which no entity’s $sort accepts. Take the member as a selector, read its name with keyOf, and build the sort with sortOf, which takes [key, direction] pairs in the order they sort:

import {
type FieldKeyOf,
type KeyMap,
keyOf,
sortOf,
type Type,
} from 'uql-orm';
function oldestFirst<E extends object, K extends FieldKeyOf<E, Date>>(
entity: Type<E>,
since: (keys: KeyMap<E>) => K,
) {
const key = keyOf(since);
return pool.findMany(entity, {
$sort: sortOf<E>([[key, 'asc']]),
$limit: 20,
});
}
const oldest = await oldestFirst(Post, (post) => post.publishedAt);

The selector follows a rename of publishedAt, where a string key would be left behind, and FieldKeyOf<E, Date> refuses a member that is not a date.