Skip to content
NewComposite primary keys5 min read

The JSON-native TypeScript ORM

UQL stands for Unified Query Language. With pure (type-safe) JSON queries, complex jobs can be done simply across SQL vendors + MongoDB. It got some inspiration from Mongo's best syntax.

A query is a plain object literal: no builder to call, no operator to import, and every key checked to the leaf. All three errors below are compile errors: a misspelled key, the same mistake three levels into the query, and an operator the column’s type rules out. None of them is written into this page; they are what tsc reports against the published uql-orm.

import { pool } from './uql.config.js';
import { User } from './entities.js';
await pool.findMany(User, {
// the entity class types every key
$select: { id: true, emial: true },
// same three levels deep
$populate: { posts: { $select: { titel: true } } },
// and operators against the column type
$where: { loginCount: { $like: 3 } },
});

None of this needs a code generation step. Entities are plain classes using the standard TC39 decorators, so there is no schema file to keep in sync, no client to regenerate, and no compiler flag to enable. The same checking reaches into JSON/JSONB dot-paths, down to a key inside a stored document. See Entities.


Same query can travel between browser, edge, and backends

Section titled “Same query can travel between browser, edge, and backends”

Given the serializable queries, the client can build and send it with all the type-safety as well. Four files, one call each - the browser call that sends it, the entity it queries, the pool it queries through, and the endpoint that exposes it:

import { HttpQuerier } from 'uql-orm/browser';
const http = new HttpQuerier('/api');
const { data } = await http.findMany(User, {
$select: { id: true, email: true },
$where: { email: { $endsWith: '@uql-orm.dev' } },
});

That handler mounts on Hono, Elysia, Next.js, Express, Bun, Deno, or Workers, and carries transactions and authorization hooks across. More on the HTTP transport and the browser client.


One package, zero runtime dependencies, 269 kB on the wire with every dialect included. The same code runs on PostgreSQL, PGlite, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun’s native SQL and MongoDB, under Node 24+, Bun, Deno, Workers, Lambda and Vercel, and the browser. It is ESM only, which rules out CommonJS projects.

Relations never hit N+1: one statement per populated relation, no matter how many rows come back, loaded eagerly so serializing the result touches nothing.

Migrations are generated from your entities and reviewed as SQL in the pull request, with drift:check to catch a database that stopped matching. raw() fits anywhere a value does when you want the SQL yourself. Semantic and vector search, tenant filters that fail closed, soft delete with restore and streaming are all included.

On a full PostgreSQL round trip UQL adds less over hand-written driver code than any other ORM we measured, by roughly 2x over the next closest, on Bun, Node and Deno alike. Full benchmark, feature-by-feature comparison, as well as type-safety comparison.