FAQ
Getting Started
Section titled “Getting Started”What is UQL, and why would I pick it?
Section titled “What is UQL, and why would I pick it?”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.
- The most portable: the same query object runs on PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, and the edge, and serializes to travel over HTTP between server and client unchanged.
- The most capable out of the box: native semantic and vector search, non-bypassable multi-tenant security filters, soft-delete with restore, and entity-first migrations, all things that are raw SQL, a plugin, or unsupported elsewhere, plus an optional REST API and typed browser client in the same package when you need them.
- The fastest ORM: adds the least over hand-written driver code of any ORM in our full PostgreSQL round trip benchmark.
- The lowest-friction: no codegen, no DSL, no build step. Your TypeScript classes are the schema.
Drizzle picks lean and fast; Prisma and TypeORM pick full-featured and heavy. UQL is built so you don’t pick.
Where does the name UQL come from?
Section titled “Where does the name UQL come from?”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.
Is UQL production-ready?
Section titled “Is UQL production-ready?”Yes. UQL runs in production behind Variability.ai, an AI meeting notetaker built by UQL’s author.
Installation & Setup
Section titled “Installation & Setup”Which database drivers do I need?
Section titled “Which database drivers do I need?”| 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:
targetmust be a dated one, neveresnext, which is where TypeScript leaves decorator syntax untransformed for an engine to reject with aSyntaxError. Every value fromes2022up emits the same thing, so drop toes2022if your TypeScript predates 6.0 and rejectses2025.modulemust benodenext(Node) orpreserve(behind a bundler), because the resolver has to read the package’sexportsmap to find subpaths likeuql-orm/postgres. Plain"module": "esnext"on TypeScript 5.x resolves nothing.libmust includeesnext, orawait usingfails onAsyncDisposable.
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.
Can I use UQL from JavaScript?
Section titled “Can I use UQL from JavaScript?”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.
Core Concepts
Section titled “Core Concepts”What does “JSON-native” mean?
Section titled “What does “JSON-native” mean?”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};Queries & Relations
Section titled “Queries & 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.
How do I join relations?
Section titled “How do I join relations?”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.
How do I filter by how many related records exist?
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.
Migrations & Schema
Section titled “Migrations & Schema”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:
# 1. Update your entity class# 2. Auto-generate the migrationnpx uql-migrate generate:entities add_user_nickname
# 3. Apply itnpx uql-migrate upCan 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:
npx uql-migrate sync --dry-run # print the DDLnpx uql-migrate sync # create the missing tables, columns, and indexesIt 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.
Can I still write manual migrations?
Section titled “Can I still write manual migrations?”Yes. Use generate for manual SQL:
npx uql-migrate generate seed_default_roles# Edit the generated filenpx uql-migrate upAdvanced Features
Section titled “Advanced Features”How does vector search work?
Section titled “How does vector search work?”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.
Is UQL safe from SQL injection?
Section titled “Is UQL safe from SQL injection?”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.
What’s the performance like?
Section titled “What’s the performance like?”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).
Troubleshooting
Section titled “Troubleshooting”Why am I getting “Decorators not working”?
Section titled “Why am I getting “Decorators not working”?”- Check
targetis notesnext(see above) - that is the one setting that silently breaks them - 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’”?”- Set
"module": "nodenext", or"preserve"behind a bundler."esnext"on TypeScript 5.x leaves the resolver unable to read the package’sexportsmap, and there is norequire()path to fall back to - Ensure
"type": "module"inpackage.jsonif Node runs the compiled output - Under
nodenext, use.jsextensions 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”?”- Verify your database is running
- Check credentials in
uql.config.ts - Ensure the database exists (
createdb your_db) - For Docker, verify network settings and port mappings
Why is my query slow?
Section titled “Why is my query slow?”- Check if you’re missing indexes on filtered columns
- Use
findManyStreamfor large result sets - Consider pagination with
$limitand$skip - Enable query logging to see the generated SQL and per-query timings