FAQ
Getting Started
Section titled “Getting Started”What is UQL and when should I use it?
Section titled “What is UQL and when should I use it?”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 SQL generation: fastest in all 8 categories of our open benchmark, including a narrow edge over the query builder Knex on batch INSERT
Why UQL?
Section titled “Why UQL?”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:
- 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: fastest in all 8 benchmark categories, ~2.1x faster on average than the next tool, quicker than raw query builders like Knex and Kysely despite them not even carrying entities or relations. Seven wins are clear (1.2x to 3.7x); batch INSERT is a narrow edge over Knex.
- 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. 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 |
Is UQL production-ready?
Section titled “Is UQL production-ready?”Yes. UQL powers Variability.ai, an AI meeting intelligence platform, in production.
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 |
| 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?”If using decorators (@Entity(), @Field()):
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true }}If using the imperative API (defineEntity), no special configuration needed.
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.
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:
{ $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:
// Recommended: cross-database portable@Field({ type: 'uuid' })
// Use rarely: exact SQL control@Field({ columnType: 'char', length: 36 })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}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 querier.findMany(Company, { $where: { 'settings.isArchived': { $ne: true }, 'settings.theme': 'dark', },});Works across all SQL dialects. UQL generates dialect-specific SQL automatically.
How do I join relations?
Section titled “How do I join relations?”await querier.findMany(Post, { $select: { id: true, title: true }, $populate: { author: { $select: { id: true, name: true } } }, $where: { author: { name: 'Roger' } }});UQL automatically generates efficient SQL with joins.
How do I count related records?
Section titled “How do I count related records?”await querier.findMany(Category, { $where: { measureUnits: { $size: { $gte: 2 } } }});Uses efficient COUNT(*) subqueries.
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 upUQL diffs your entities against the live database and generates the exact SQL.
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 querier.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.
What’s the performance like?
Section titled “What’s the performance like?”In our open benchmark of SQL-generation speed, UQL is the fastest entry in all 8 query categories, on average ~2.1× faster than the runner-up (seven wins are clear at 1.2× to 3.7×; batch INSERT is a narrow edge over the pure query builder Knex). 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
Troubleshooting
Section titled “Troubleshooting”Why am I getting “Decorators not working”?
Section titled “Why am I getting “Decorators not working”?”- Ensure
experimentalDecoratorsandemitDecoratorMetadataintsconfig.json - Set
"module": "NodeNext"or"module": "ESNext" - Ensure
"type": "module"inpackage.json - Use
.jsextensions in imports (TypeScript resolves to.ts)
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