Skip to content
NewComposite primary keys6 min read

The JSON-native TypeScript ORM

UQL (Unified Query Language) queries SQL databases and MongoDB with plain, type-safe JSON, in a syntax inspired by MongoDB's.

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”

Queries are serializable, so the browser can build and send one with the same type-safety. Four files, one call each: the browser call, the entity, the pool, 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 and every dialect included, yet uql-orm/postgres is about 27 kB gzipped. The same code runs on PostgreSQL, PGlite, CockroachDB, MySQL, MariaDB, MSSQL, 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: the whole graph is one statement, however 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, on Bun, Node and Deno alike. Full benchmark, feature-by-feature comparison, as well as type-safety comparison.