Skip to content
NewComposite primary keys5 min read

FAQ

One design decision is behind everything else: a UQL query is plain data with the best type-safety, not a compiled method chain. That is what lets it be four things most ORMs treat as trade-offs.

Drizzle picks lean and fast; Prisma and TypeORM pick full-featured and heavy. UQL is built so you don’t pick.

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. That is the first of the five things a perfect ORM should have, and the reason the same query object runs on the server, on the edge, and in the browser.

How is UQL different from Drizzle, Prisma, or TypeORM?

Section titled “How is UQL different from Drizzle, Prisma, or TypeORM?”
Feature UQL Prisma Drizzle TypeORM
Query format JSON object Object literal Function chains Method chains
Codegen None needed Required None None
Multi-DB API One syntax Mostly consistent Per-dialect schemas Diverges for MongoDB
Browser queries Built-in Not supported Manual Manual
Vector search Native operator Via extension Via extension Raw SQL
Query filters / scopes Built-in (@Filter) Via extension Manual Soft-delete only
Multi-tenancy / RLS Built-in (security filters) Via extension Manual Manual

Every operation side by side, MikroORM included, is on the comparison page.

Yes. UQL runs in production behind Variability.ai, an AI meeting notetaker built by UQL’s author.


Database Driver Package
PostgreSQL pg
PGlite @electric-sql/pglite
MySQL mysql2
MariaDB mariadb or mysql2
SQLite better-sqlite3
CockroachDB pg
LibSQL / Turso @libsql/client
MongoDB mongodb
Neon @neondatabase/serverless
Bun SQL Native Built-in (no install)
Cloudflare D1 Built-in Workers binding (no install)

For Bun, you don’t need external drivers. Bun’s native SQL supports PostgreSQL, MySQL, and SQLite out of the box.

Do I need special TypeScript configuration?

Section titled “Do I need special TypeScript configuration?”

No decorator flags. UQL uses the standard TC39 decorators, so neither experimentalDecorators nor emitDecoratorMetadata is involved, and there is no polyfill to install, for either the decorator or the imperative (defineEntity) style.

Three settings do matter, and they are the same three listed under Requirements:

  • target must be a dated one, never esnext, which is where TypeScript leaves decorator syntax untransformed for an engine to reject with a SyntaxError. Every value from es2022 up emits the same thing, so drop to es2022 if your TypeScript predates 6.0 and rejects es2025.
  • module must be nodenext (Node) or preserve (behind a bundler), because the resolver has to read the package’s exports map to find subpaths like uql-orm/postgres. Plain "module": "esnext" on TypeScript 5.x resolves nothing.
  • lib must include esnext, or await using fails on AsyncDisposable.

UQL ships as ESM only, so Node also needs "type": "module" in package.json to run the compiled output. Bun, Deno and bundler-driven frameworks do not.

Yes, through defineEntity. Nothing in UQL reads TypeScript types at runtime, so a plain class registers the same metadata a decorated one does, and the CLI reads a uql.config.js as happily as a .ts one.

Decorators are the exception, and not because of UQL: no JavaScript engine implements them yet, so Node, Deno and the browser all reject the syntax in a .js file. TypeScript compiles them away, which is why nobody writing .ts meets this. In JavaScript something has to do that same job: Bun transpiles every file it runs, so they work there as they are, and anywhere else it takes Babel, SWC or esbuild. defineEntity needs none of it.


A UQL query is a plain JavaScript object:

import type { Query } from 'uql-orm/type';
import { User } from './shared/models/index.js';
const query: Query<User> = {
$select: { id: true, name: true },
$where: { email: { $endsWith: '@uql-orm.dev' } },
$sort: { createdAt: 'desc' },
$limit: 10,
};

Because the query is data rather than code, you can JSON.stringify() it and send it over HTTP, cache it, diff it programmatically, or share it between backend and frontend.

How do I expose entities over HTTP or query from the browser?

Section titled “How do I expose entities over HTTP or query from the browser?”

The HTTP transport core serves your entities as a REST API from any framework (Express, Hono, Next.js, Bun, Workers, …), with hooks for auth and tenant scoping. On the frontend, HttpQuerier consumes that API with the same type-safe query syntax you use on the backend.

What’s the difference between type and columnType?

Section titled “What’s the difference between type and columnType?”

Use type for portability, columnType for precise SQL control. type is always required (it is what the compiler checks the property against); columnType overrides only the SQL type it maps to:

import { Field } from 'uql-orm';
// Recommended: cross-database portable
@Field({ type: 'uuid' })
externalId?: string;
// Use rarely: exact SQL control
@Field({ type: String, columnType: 'char', length: 36 })
externalId?: string;

type: 'uuid' generates UUID on Postgres but CHAR(36) on MySQL automatically.

What’s the difference between $select and $populate?

Section titled “What’s the difference between $select and $populate?”
  • $select: Scalar fields (strings, numbers, dates, JSON)
  • $populate: Related entities (relations)
const query: Query<User> = {
$select: { id: true, name: true }, // scalar fields
$populate: { posts: { $select: { title: true } } }, // relations
};

How do I filter by nested JSON properties?

Section titled “How do I filter by nested JSON properties?”

Use dot-notation paths in $where:

await pool.findMany(Company, {
$where: {
'settings.isArchived': { $ne: true },
'settings.theme': 'dark',
},
});

Works the same way on every SQL dialect UQL supports.

await pool.findMany(Post, {
$select: { id: true, title: true },
$populate: {
author: { $select: { id: true, name: true } },
},
$where: { author: { name: 'Roger' } },
});

One query with a JOIN, not one query per row.

Section titled “How do I filter by how many related records exist?”
await pool.findMany(MeasureUnitCategory, {
$where: {
measureUnits: { $size: { $gte: 2 } },
},
});

$size compiles to a COUNT(*) subquery, so “categories with at least 2 measure units” never has to load the relation to check.


Do I need to write SQL migrations manually?

Section titled “Do I need to write SQL migrations manually?”

No. UQL uses an Entity-First approach:

Terminal window
# 1. Update your entity class
# 2. Auto-generate the migration
npx uql-migrate generate:entities add_user_nickname
# 3. Apply it
npx uql-migrate up

Can UQL create the database from my entities?

Section titled “Can UQL create the database from my entities?”

Yes, and it is the same diff the migration generator uses, applied directly:

Terminal window
npx uql-migrate sync --dry-run # print the DDL
npx uql-migrate sync # create the missing tables, columns, and indexes

It only adds, so it will not drop a column or alter a type. Use it on a prototype or a test database, and a generated migration once there is data you would miss. For the reverse direction, generate:from-db writes entity classes from tables that already exist.

Yes. Use generate for manual SQL:

Terminal window
npx uql-migrate generate seed_default_roles
# Edit the generated file
npx uql-migrate up

import type { WithDistance } from 'uql-orm';
const results = (await pool.findMany(Article, {
$sort: {
embedding: {
$vector: queryEmbedding,
$distance: 'cosine',
$project: 'distance',
},
},
$limit: 10,
})) as WithDistance<Article, 'distance'>[];

Works on PostgreSQL and PGlite (pgvector), CockroachDB, MariaDB, SQLite (sqlite-vec), and MongoDB Atlas, all with the same query syntax. See Semantic Search.

Does UQL support soft delete, restore, and multi-tenancy?

Section titled “Does UQL support soft delete, restore, and multi-tenancy?”

Yes. Mark a field with @Field({ softDelete: true }) and deletes soft-delete automatically, reads hide trashed rows, and restoreOneById / restoreMany bring them back ({ hardDelete: true } removes for good).

Soft-delete is one instance of UQL’s general query filters, which are named, default-on $where fragments. Mark a filter security and resolve it from a per-request context and you have multi-tenancy / row-level security: applied to every query, relations and cascades included, non-bypassable from the client, and fail-closed when the context is missing.

Yes, and every value in a query is bound as a parameter ($1 on Postgres-wire dialects, ? on MySQL-family ones) and handed to the driver separately, so nothing you pass through $where, $select, or an insert is ever concatenated into the statement. Table and column names come from your entity metadata rather than from the query, so they cannot be injected either.

The one exception is the programmatic raw, a deliberate opt-out. It is a tagged template, so the literal is yours to control and every interpolation is bound:

import { raw } from 'uql-orm';
raw`"stock" - ${quantity}`;

Its @deprecated string form and its callback form do not bind, so build neither from user input; inside a callback, bind with ctx.addValue().

UQL binds parameters but issues no server-side prepared statements, which is also why transaction-mode poolers such as Supabase’s work with no extra configuration.

In our open benchmark, which times a full PostgreSQL lifecycle, UQL adds less over hand-written driver code than any other ORM measured. Two design choices drive this:

  • Schema metadata (tables, columns, relations) is pre-computed once at startup
  • SQL is written directly into a string buffer, avoiding intermediate objects (only the statement text; dynamic values are bound safely, never interpolated).

Why am I getting “Decorators not working”?

Section titled “Why am I getting “Decorators not working”?”
  1. Check target is not esnext (see above) - that is the one setting that silently breaks them
  2. If your bundler transforms TypeScript itself, confirm it implements standard decorators: esbuild, SWC, Babel (version: '2023-11') and Bun all do, but Oxc (Vite 8’s own transformer) does not, so a Vite 8 build needs one of the others via a plugin

Why am I getting “Cannot find module ‘uql-orm’”?

Section titled “Why am I getting “Cannot find module ‘uql-orm’”?”
  1. Set "module": "nodenext", or "preserve" behind a bundler. "esnext" on TypeScript 5.x leaves the resolver unable to read the package’s exports map, and there is no require() path to fall back to
  2. Ensure "type": "module" in package.json if Node runs the compiled output
  3. Under nodenext, use .js extensions in relative imports (TypeScript resolves them to .ts)

Why am I getting “Cannot find global type ‘AsyncDisposable’”?

Section titled “Why am I getting “Cannot find global type ‘AsyncDisposable’”?”

await using needs those typings and no dated target pulls them in on its own. Add "lib": ["esnext"].

Why am I getting “Connection refused”?

Section titled “Why am I getting “Connection refused”?”
  1. Verify your database is running
  2. Check credentials in uql.config.ts
  3. Ensure the database exists (createdb your_db)
  4. For Docker, verify network settings and port mappings
  1. Check if you’re missing indexes on filtered columns
  2. Use findManyStream for large result sets
  3. Consider pagination with $limit and $skip
  4. Enable query logging to see the generated SQL and per-query timings