Skip to content

FAQ

UQL is an ORM for TypeScript that offers:

  • Serializable queries: plain JSON objects you can cache, send over HTTP, or store
  • No codegen: your TypeScript classes are the schema, no build step needed
  • One API everywhere: the same syntax works on PostgreSQL, MySQL, MongoDB, SQLite, and edge runtimes
  • Fast in practice: adds the least over hand-written driver code of any ORM in our open benchmark

One design decision is behind it: a UQL query is plain data, not a compiled method chain. That is what lets UQL 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. See the full comparison.

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

Section titled “How is UQL different from Prisma, Drizzle, 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

Yes. UQL powers Variability.ai, an AI meeting intelligence platform, in production.


Database Driver Package
PostgreSQL pg
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. UQL uses the most-modern standard TC39 decorators, so there are no compiler flags to enable and no polyfill to install, for either the decorator or the imperative (defineEntity) style.

The one setting that matters is target: it must not be esnext, because at that target TypeScript emits decorator syntax untransformed and no JavaScript engine implements decorators yet. Any dated target (es2022 through es2025) is fine.

UQL ships as ESM only: Node projects need "module": "NodeNext" in tsconfig.json and "type": "module" in package.json; bundler-based projects (Next.js, Vite) work with their defaults.


A UQL query is a plain JavaScript object:

{
$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)
{
$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(Category, {
$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

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 (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 - named, default-on $where fragments. Mark a filter security and resolve it from a per-request context and you get 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.

In our open benchmark, which times a full PostgreSQL lifecycle, UQL adds 239µs over hand-written driver code, the least of any ORM. 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

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. Set "module": "NodeNext" or "module": "ESNext"
  3. Ensure "type": "module" in package.json
  4. Use .js extensions in imports (TypeScript resolves to .ts)
  5. 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 build needs one of the others via a plugin

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