UQL is the smartest ORM for TypeScript. It is engineered to be fast, safe, and universally compatible.
- Runs Everywhere: Node.js, Bun, Deno, Cloudflare Workers, Electron, React Native, and the Browser.
- Unified API: A consistent query interface for PostgreSQL (incl. CockroachDB, YugabyteDB), MySQL (incl. TiDB, Aurora), MariaDB, SQLite, LibSQL, Neon, Cloudflare D1, and MongoDB.
const users = await querier.findMany(User, {
$select: { email: true, profile: { $select: { picture: true } } },
$where: { email: { $endsWith: '@domain.com' } },
$sort: { createdAt: 'desc' },
$limit: 100,
});
| Feature | UQL | Traditional ORMs |
|---|---|---|
| API | Unified & Intuitive: Same syntax for SQL & NoSQL. | Fragmented: SQL and Mongo feel like different worlds. |
| Safety | Deep Type-Safety: Validates relations & operators at any depth. | Surface-level: Often loses types in complex joins. |
| Syntax | Serializable JSON: Pure data, perfect for APIs/Websockets. | Method-Chaining: Hard to transport over the wire. |
| Efficiency | Sticky Connections: Minimal overhead, human-readable SQL. | Heavy: Often generates "SQL Soup" that's hard to debug. |
| Feature | Description |
|---|---|
| Context-Aware Queries | Deep type-safety for operators and relations at any depth. |
| Serializable JSON | 100% valid JSON queries for easy transport over HTTP/Websockets. |
| Unified Dialects | Write once, run anywhere: PostgreSQL, MySQL, SQLite, MongoDB, and more. |
| Naming Strategies | Pluggable system to translate between TypeScript camelCase and database snake_case. |
| Smart SQL Engine | Optimized sub-queries, placeholders ($1, $2), and minimal SQL generation via QueryContext. |
| Thread-Safe by Design | Centralized task queue and @Serialized() decorator prevent race conditions. |
| Declarative Transactions | Standard @Transactional() and @InjectQuerier() decorators for NestJS/DI. |
| Modern & Versatile | Pure ESM, high-res timing, Soft-delete, and Vector/JSONB/JSON support. |
| Database Migrations | Built-in Entity-First synchronization and a robust CLI for version-controlled schema evolution. |
| Logging & Monitoring | Professional-grade monitoring with slow-query detection and colored output. |
Install the core package and the driver for your database:
# Core
npm install @uql/core # or bun add / pnpm add| Database | Command |
|---|---|
| PostgreSQL (incl. Neon, Cockroach, Yugabyte) | npm install pg |
| MySQL (incl. TiDB, Aurora) | npm install mysql2 |
| MariaDB | npm install mariadb |
| SQLite | npm install better-sqlite3 |
| LibSQL (incl. Turso) | npm install @libsql/client |
| MongoDB | npm install mongodb |
| Cloudflare D1 | Native (no driver needed) |
Ensure your tsconfig.json is configured to support decorators and metadata:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "NodeNext",
"target": "ESNext"
}
} Note: ES2020+ will work for target as well.
Annotate your classes with decorators. UQL's engine uses this metadata for both type-safe querying and precise DDL generation.
| Decorator | Purpose |
|---|---|
@Entity() |
Marks a class as a database table/collection. |
@Id() |
Defines the Primary Key with support for onInsert generators (UUIDs, etc). |
@Field() |
Standard column. Use { reference: ... } for Foreign Keys. |
@OneToOne |
Defines a one-to-one relationship. |
@OneToMany |
Defines a one-to-many relationship. |
@ManyToOne |
Defines a many-to-one relationship. |
@ManyToMany |
Defines a many-to-many relationship. |
@Virtual() |
Defines a read-only field calculated via SQL (see Advanced). |
UQL separates the intent of your data from its storage. Both properties are optional; if omitted, UQL performs a best-effort inference using the TypeScript types from your class (provided emitDecoratorMetadata is enabled).
| Property | Purpose | Values |
|---|---|---|
type |
Logical Type (Abstraction). Used for runtime behavior and automatic SQL mapping. | String, Number, Boolean, Date, BigInt, or semantic strings: 'uuid', 'json', 'vector'. |
columnType |
Physical Type (Implementation). Highest Priority. Bypasses UQL's inference for exact SQL control. | Raw SQL types: 'varchar(100)', 'decimal(10,2)', 'smallint', etc. |
@Field() // Inference: Maps to TEXT (Postgres) or VARCHAR(255) (MySQL) automatically.
name?: string;
@Field({ type: 'uuid' }) // Recommended: Cross-database abstraction for UUIDs.
id?: string;
@Field({ columnType: 'varchar(500)' }) // Control: Explicitly forces a specific SQL type.
bio?: string;
import { v7 as uuidv7 } from 'uuid';
import { Entity, Id, Field, OneToOne, OneToMany, ManyToOne, ManyToMany, type Relation } from '@uql/core';
@Entity()
export class User {
@Id({ type: 'uuid', onInsert: () => uuidv7() })
id?: string;
@Field({
index: true,
})
name?: string;
@Field({
unique: true,
comment: 'User login email',
})
email?: string;
@OneToOne({
entity: () => Profile,
mappedBy: (profile) => profile.user,
cascade: true,
})
profile?: Relation<Profile>;
@OneToMany({
entity: () => Post,
mappedBy: (post) => post.author,
})
posts?: Relation<Post>[];
}
@Entity()
export class Profile {
@Id({ type: 'uuid', onInsert: () => uuidv7() })
id?: string;
@Field()
bio?: string;
@Field({ reference: () => User, foreignKey: 'fk_profile_user' })
userId?: string;
@OneToOne({ entity: () => User })
user?: User;
}
@Entity()
export class Post {
@Id()
id?: number;
@Field()
title?: string;
@Field({ reference: () => User })
authorId?: string;
@ManyToOne({ entity: () => User })
author?: User;
@ManyToMany({
entity: () => Tag,
through: () => PostTag,
})
tags?: Tag[];
}
@Entity()
export class Tag {
@Id({ type: 'uuid', onInsert: () => uuidv7() })
id?: string;
@Field()
name?: string;
}
@Entity()
export class PostTag {
@Id({ type: 'uuid', onInsert: () => uuidv7() })
id?: string;
@Field({ reference: () => Post })
postId?: number;
@Field({ reference: () => Tag })
tagId?: string;
}Pro Tip: Use the
Relation<T>utility type for relationship properties. It prevents TypeScript circular dependency errors while maintaining full type-safety.
A pool manages connections (queriers). Initialize it once at application bootstrap (e.g., in server.ts).
import { SnakeCaseNamingStrategy, type Config } from '@uql/core';
import { PgQuerierPool } from '@uql/core/postgres'; // or mysql2, sqlite, etc.
import { User, Profile, Post } from './entities';
export const pool = new PgQuerierPool(
{ host: 'localhost', database: 'uql_app', max: 10 },
{
logger: ['error', 'warn', 'migration'],
slowQueryThreshold: 1000,
namingStrategy: new SnakeCaseNamingStrategy()
}
);
export default {
pool,
entities: [User, Profile, Post],
migrationsPath: './migrations',
} satisfies Config;Pro Tip: Reusing the same connection pool for both your application and migrations is recommended. It reduces connection overhead and ensures consistent query settings (like naming strategies).
UQL provides a straightforward API to interact with your data. Always ensure queriers are released back to the pool.
const querier = await pool.getQuerier();
try {
const users = await querier.findMany(User, {
$select: {
name: true,
profile: { $select: ['bio'], $required: true } // INNER JOIN
},
$where: {
status: 'active',
name: { $istartsWith: 'a' } // Case-insensitive search
},
$limit: 10,
$skip: 0
});
} finally {
await querier.release(); // Essential for pool health
}Generated SQL (PostgreSQL):
SELECT "User"."name", "profile"."id" AS "profile_id", "profile"."bio" AS "profile_bio"
FROM "User"
INNER JOIN "Profile" AS "profile" ON "profile"."userId" = "User"."id"
WHERE "User"."status" = 'active' AND "User"."name" ILIKE 'a%'
LIMIT 10 OFFSET 0
Define complex logic directly in your entities using raw functions. These are resolved during SQL generation for peak efficiency.
@Entity()
export class Item {
@Field({
virtual: raw(({ ctx, dialect, escapedPrefix }) => {
ctx.append('(');
dialect.count(ctx, ItemTag, {
$where: { itemId: raw(({ ctx }) => ctx.append(`${escapedPrefix}.id`)) }
}, { autoPrefix: true });
ctx.append(')');
})
})
tagsCount?: number;
}
UQL is one of the few ORMs with a centralized serialization engine. Transactions are guaranteed to be race-condition free.
const result = await pool.transaction(async (querier) => {
const user = await querier.findOne(User, { $where: { email: '...' } });
await querier.insertOne(Profile, { userId: user.id, bio: '...' });
});Perfect for NestJS and other Dependency Injection frameworks. Use @Transactional() to wrap a method and @InjectQuerier() to access the managed connection.
import { Transactional, InjectQuerier, type Querier } from '@uql/core';
export class UserService {
@Transactional()
async register({picture, ...user}: UserProfile, @InjectQuerier() querier?: Querier) {
const userId = await querier.insertOne(User, user);
await querier.insertOne(Profile, { userId, picture });
}
}
Ideally, use the same uql.config.ts for your application bootstrap and the CLI:
// uql.config.ts
import type { Config } from '@uql/core';
export default {
pool: new PgQuerierPool({ /* ... */ }),
entities: [User, Profile, Post],
migrationsPath: './migrations',
} satisfies Config;Why? Using a single config for both your app and the CLI is recommended for consistency. It prevents bugs where your runtime uses one naming strategy (e.g. camelCase) but your migrations use another (e.g. snake_case), or where the CLI isn't aware of all your entities. It enforces a Single Source of Truth for your database connection and schema.
Use the CLI to manage your database schema evolution.
| Command | Description |
|---|---|
generate <name> |
Creates an empty timestamped file for manual SQL migrations (e.g., data backfills). |
generate:entities <name> |
Auto-generates a migration by diffing your entities against the current DB schema. |
up |
Applies all pending migrations. |
down |
Rolls back the last applied migration batch. |
status |
Shows which migrations have been executed and which are pending. |
# 1. Create a manual migration
npx uql-migrate generate seed_default_roles
# 2. Auto-generate schema changes from your code
npx uql-migrate generate:entities add_profile_table
# 3. Apply changes
npx uql-migrate up
# 4. Revert changes if needed
npx uql-migrate down
# 5. Run with custom config
npx uql-migrate up --config ./configs/uql.config.tsBun Users: If your
uql.config.tsuses TypeScript path aliases (e.g.,~app/...), run migrations with the--bunflag to ensure proper resolution:bun run --bun uql-migrate statusOr add a script to your
package.json:"uql": "bun run --bun uql-migrate", then run commands like, e.g.,bun run uql status.
Keep your schema in sync without manual migrations. It is Safe by Default: In safe mode (default), it strictly adds new tables and columns but blocks any destructive operations (column drops or type alterations) to prevent data loss. It provides Transparent Feedback by logging detailed warnings for any blocked changes, so you know exactly what remains to be migrated manually.
Important: For
autoSyncto detect your entities, they must be loaded (imported) before callingautoSync.
Using Your Config (Recommended)
If you follow the unified configuration pattern, your entities are already imported. Simply reuse it:
import { Migrator } from '@uql/core/migrate';
import config from './uql.config.js';
const migrator = new Migrator(config.pool, {
entities: config.entities,
});
await migrator.autoSync({ logging: true });Explicit Entities
Alternatively, pass entities directly if you want to be explicit about which entities to sync:
import { Migrator } from '@uql/core/migrate';
import { User, Profile, Post } from './entities/index.js';
const migrator = new Migrator(pool, {
entities: [User, Profile, Post],
});
await migrator.autoSync({ logging: true });
UQL features a professional-grade, structured logging system designed for high visibility and sub-millisecond performance monitoring.
| Level | Description |
|---|---|
query |
Standard Queries: Beautifully formatted SQL/Command logs with execution time. |
slowQuery |
Bottleneck Alerts: Dedicated logging for queries exceeding your threshold. |
error / warn |
System Health: Detailed error traces and potential issue warnings. |
migration |
Audit Trail: Step-by-step history of schema changes. |
skippedMigration |
Safety: Logs blocked unsafe schema changes during autoSync. |
schema / info |
Lifecycle: Informative logs about ORM initialization and sync events. |
The DefaultLogger provides high-contrast, colored output out of the box:
query: SELECT * FROM "user" WHERE "id" = $1 -- [123] [2ms]
slow query: UPDATE "post" SET "title" = $1 -- ["New Title"] [1250ms]
error: Failed to connect to database: Connection timeout
Pro Tip: Even if you disable general query logging in production (
logger: ['error', 'warn', 'slowQuery']), UQL stays silent until a query exceeds your threshold.
Learn more about UQL at uql.app for details on:
- Complex Logical Operators
- Relationship Mapping (1-1, 1-M, M-M)
- Soft Deletes & Auditing
- Database Migration & Syncing
For those who want to see the "engine under the hood," check out these resources in the source code:
- Entity Mocks: See how complex entities and virtual fields are defined in entityMock.ts.
- Core Dialect Logic: The foundation of our context-aware SQL generation in abstractSqlDialect.ts.
- Comprehensive Test Suite:
- Abstract SQL Spec: Base test suite for all dialects.
- PostgreSQL | MySQL | SQLite specs.
- Querier Integration Tests: SQL generation & connection management tests.
UQL is an open-source project proudly sponsored by Variability.ai.