Skip to content
NewFrom Mongoose to SQL6 min read

FAQ

UQL is a TypeScript ORM whose queries are plain, typed JSON objects instead of method chains. Your classes are the schema, with no codegen or build step. One query runs on PostgreSQL, CockroachDB, MySQL, MariaDB, SQL Server, the SQLite family (libSQL, Turso, D1) and MongoDB, and travels over HTTP to the browser unchanged.

It ships vector search, multi-tenant filters, soft delete, migrations, a REST API and a browser client in one package, and adds the least measured time and memory over the driver in our PostgreSQL benchmark. The comparison page sets it beside Drizzle, MikroORM, Prisma and TypeORM.

Unified Query Language: one typed JSON syntax, modeled on MongoDB’s, for SQL databases and MongoDB alike. It is the first of five things a perfect ORM should have.

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

Your database’s own: pg, mysql2, mariadb, better-sqlite3, mssql, mongodb, and so on. Which pool pairs each entry point with its driver. On Bun, bun:sql (PostgreSQL, CockroachDB, MySQL, MariaDB) and bun:sqlite need none, and D1 takes the Worker’s binding.

No decorator flags or polyfills: UQL uses standard TC39 decorators. module, target and lib matter, as Requirements explains.

Yes, with defineEntity, which registers the same metadata as decorators. The CLI reads a uql.config.js like a .ts one. Decorators in a .js file need Bun or a transpiler such as Babel, SWC or esbuild.

A query is a plain object, so you can JSON.stringify() it, send it over HTTP, cache it, or share it between backend and frontend:

import type { Query } from 'uql-orm';
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,
};

The HTTP transport serves your entities as a REST API, and HttpQuerier sends these queries to it from the browser.

What is the difference between $select and $populate?

Section titled “What is the difference between $select and $populate?”

$select picks scalar fields. $populate loads relations, in the same statement:

const query: Query<User> = {
$select: { id: true, name: true },
$populate: { posts: { $select: { title: true } } },
};

How do I filter on a relation or a JSON property?

Section titled “How do I filter on a relation or a JSON property?”

A relation filters by its fields, and a JSON column by dot-paths. Both work on every SQL dialect:

await pool.findMany(Post, { $where: { author: { name: 'Roger' } } });
await pool.findMany(Company, { $where: { 'settings.theme': 'dark' } });

$size filters by how many related rows exist, as a COUNT(*) subquery that never loads them:

await pool.findMany(MeasureUnitCategory, {
$where: { measureUnits: { $size: { $gte: 2 } } },
});

What is the difference between type and columnType?

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

type is the portable type the compiler checks the property against, and every field states one, except a foreign key, which takes references and inherits the target key’s type, and a relation aggregate, which its aggregate types. columnType overrides the SQL type it maps to. See Column types.

Yes. Every value is bound as a parameter, and table and column names come from entity metadata, never the query.

sql is the deliberate opt-out. As a tagged template it binds every interpolated value, and columns come from refs:

import { sql, refs } from 'uql-orm';
const item = refs(Item);
sql`${item.stock} - ${quantity}`;

Its callback form does not bind, so never build one from user input. Bind values there with ctx.addValue().

UQL does not use server-side prepared statements, so transaction-mode poolers such as Supabase’s work without extra configuration.

No. Edit the entity, run npx uql-migrate generate:entities <name> to write a migration from the diff, and up to apply it. For your own SQL, generate <name> creates an empty migration. See Migrations.

Can UQL create the database from my entities?

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

Yes, with sync:

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

By default sync only adds: it drops no column and changes no type unless you pass --unsafe. Use it on prototypes and test databases, and generated migrations once the data matters. generate:from-db goes the other way, from tables to entities.

Should I use a lifecycle hook or a trigger?

Section titled “Should I use a lifecycle hook or a trigger?”

A lifecycle hook runs in your app on UQL’s own writes, and can call a queue or another service. A trigger runs in the database, so it also fires on raw SQL, data migrations and other services’ writes. Use a trigger for data every write must produce, such as audit rows, and a hook for application logic. Triggers are SQL-only: on MongoDB, a write to an entity that declares one is refused.

How do soft delete and multi-tenancy work?

Section titled “How do soft delete and multi-tenancy work?”

Mark a field @Field({ softDelete: true }). Deletes then set it, reads hide those rows, restoreOneById and restoreMany bring them back, and { hardDelete: true } removes them for good.

Soft delete is one query filter, a named $where that applies by default. A security filter resolved from a per-request context gives you multi-tenancy: it covers every query, relations and cascades included, a client cannot bypass it, and it fails closed without a context.

Declare @Field({ type: 'vector', dimensions }) and sort with $vector. It runs on PostgreSQL and PGlite (pgvector), CockroachDB, MariaDB, SQLite (sqlite-vec), libSQL/Turso, SQL Server 2025 and MongoDB Atlas. Start with AI & RAG; Semantic Search is the reference.

  • target must not be esnext (Requirements).
  • A bundler that compiles TypeScript itself must support standard decorators. esbuild, SWC, Babel (version: '2023-11') and Bun do. Oxc, Vite 8’s own transformer, does not: add a plugin for one of the others.
  • Next.js compiles legacy decorators only, so it needs a Babel config.
  • On Node, uql-migrate needs tsx to import a TypeScript config (Running the CLI).

Set "module": "nodenext", or "preserve" behind a bundler, so TypeScript reads the package’s exports. If Node runs the output, set "type": "module" in package.json. Under nodenext, relative imports end in .js.

“Cannot find global type ‘AsyncDisposable’”

Section titled ““Cannot find global type ‘AsyncDisposable’””

Add "lib": ["esnext"]. await using needs it, and no dated target’s default lib has it.

Check that the database is running and exists (createdb your_db), the credentials in uql.config.ts, and on Docker, the network and port mappings.

Index the columns you filter by, page with $limit and $skip, and use findManyStream for large reads. Query logging shows the SQL and its timing.