> Every UQL docs page, as Markdown: https://uql-orm.dev/llms.txt
> The same docs over MCP: https://uql-orm.dev/mcp
> Before writing UQL code, read the skill: https://uql-orm.dev/.well-known/agent-skills/uql-orm/SKILL.md

# Switching to UQL

> Scaffold entities from the database you already run, translate the queries you already write, and move a production system over in phases.

Source: https://uql-orm.dev/switching-to-uql

UQL runs in the same process as Drizzle, MikroORM, Mongoose, Prisma or TypeORM, so you can move one endpoint at a time.

The steps: [scaffold entities](#step-1-scaffold-entities-from-the-database-you-have) from your database, [run UQL beside your ORM](#step-2-run-it-beside-your-current-orm), [translate your queries](#translating-what-you-already-write), then [move traffic in phases](#migrating-in-phases). [Habits to unlearn](#habits-to-unlearn) are at the end.

## Step 1: Scaffold entities from the database you have

Point the CLI at your database and it writes the `@Entity` classes, with relations taken from the foreign keys:

```bash
npx uql-migrate generate:from-db --output ./src/entities
```

It needs a config with a pool. If your columns are `snake_case` and your code is `camelCase`, set the [naming strategy](https://uql-orm.dev/naming-strategy.md) there; it applies to queries and generated DDL alike:

```ts title="uql.config.ts"
import { SnakeCaseNamingStrategy, type Config } from 'uql-orm';
import { PgQuerierPool } from 'uql-orm/postgres';

const pool = new PgQuerierPool(
  { connectionString: process.env.DATABASE_URL },
  { namingStrategy: new SnakeCaseNamingStrategy() },
);

export default { pool, migrationsPath: './migrations' } satisfies Config;
export { pool };
```

Then check the result against the database:

```bash
npx uql-migrate drift:check
```

Keep it in CI to catch entities and database drifting apart.

Relations come only from foreign key constraints, so a junction table without them arrives as a plain entity. See [scaffolding](https://uql-orm.dev/migrations.md#from-a-database-to-entities) and [drift detection](https://uql-orm.dev/migrations.md#drift-detection).

After this, the entities lead: edit a class and [`generate:entities`](https://uql-orm.dev/migrations.md#from-entities-to-the-database) writes the migration to match.

## Step 2: Run it beside your current ORM

The two share nothing. You do get a second connection pool, so lower each pool’s max to stay within the database’s connection limit.

Each call on [UQL’s pool](https://uql-orm.dev/querying/querier.md#choosing-poolx-vs-querierx) (`pool.findMany`, `pool.insertOne`) takes its own connection, as in Prisma and TypeORM. Use `pool.withQuerier` or `pool.transaction` when statements must share a connection or commit together.

A transaction cannot span both ORMs, so move a whole unit of work at once.

## The shift in mental model

Each ORM has one habit that does not carry over. The rest is renaming.

### From Prisma: no schema file, no codegen

Prisma generates a client from a `.prisma` file, which adds a build step and can fall out of sync with your code.

- **Prisma:** edit `.prisma`, run `npx prisma generate`, then use the generated client.
- **UQL:** edit the `@Entity` class, then query it. The class *is* the schema, and the [standard decorators](https://uql-orm.dev/entities/basic.md) are type-checked against their properties.

### From Drizzle: a declarative object instead of composed SQL

In Drizzle every condition is an imported function (`eq()`, `and()`, `sql`). UQL queries are plain JSON, so they can also travel over the network.

- **Drizzle:** `db.select().from(users).where(and(eq(users.id, 1), gte(users.age, 18)))`
- **UQL:** `pool.findMany(User, { $where: { id: 1, age: { $gte: 18 } } })`

### From MikroORM or TypeORM: explicit mutations instead of managed state

A Unit of Work tracks loaded entities and flushes changes for you, at the cost of detached-entity errors and writes you did not ask for. UQL tracks nothing: a write happens only where you call one.

- **Managed:** `user.name = 'New Name'; await em.flush();`
- **UQL:** `await pool.updateOneById(User, id, { name: 'New Name' });`

### From Mongoose: keep the query style, gain SQL

[UQL’s operators](https://uql-orm.dev/querying/comparison-operators.md) are Mongoose’s (`$gte`, `$in`, `$regex`, `$elemMatch`, `$or`). A document becomes an `@Entity` class. The same query runs on MongoDB *and* every SQL engine, so you can move off Mongo one table at a time.

- **Mongoose:** `User.find({ status: 'active' }).sort('-createdAt').limit(10)`
- **UQL:** `pool.findMany(User, { $where: { status: 'active' }, $sort: { createdAt: 'desc' }, $limit: 10 })`

## Translating what you already write

### Method and operator equivalents

Every method below exists on the pool and on a [querier](https://uql-orm.dev/querying/querier.md), with the same arguments.

Prisma:

| Prisma | UQL |
| - | - |
| `findMany({ where, select, orderBy, take, skip })` | `findMany(User, { $where, $select, $sort, $limit, $skip })` |
| `findFirst({ where })` | `findOne(User, { $where })` |
| `findUnique({ where: { id } })` | `findOneById(User, id)` |
| `count({ where })` | `count(User, { $where })` |
| `groupBy` / `aggregate` | [`aggregate(User, { $group, $select, $having })`](https://uql-orm.dev/querying/aggregate.md) |
| `create({ data })` | `insertOne(User, data)`, returns the id |
| `createMany({ data })` | `insertMany(User, data)`; returns an id per row |
| `update({ where: { id }, data })` | `updateOneById(User, id, data)` |
| `updateMany({ where, data })` | `updateMany(User, { $where }, data)` |
| `upsert({ where, create, update })` | `upsertOne(User, conflictPaths, data)` |
| `delete` / `deleteMany` | `deleteOneById` / `deleteMany` |
| `include` / nested `select` | [`$populate`](https://uql-orm.dev/querying/relations.md) |
| `$transaction(fn)` | [`pool.transaction(fn)`](https://uql-orm.dev/querying/transactions.md) |
| `$queryRaw` / `$executeRaw` | [`all(sql, values)`](https://uql-orm.dev/querying/raw-sql.md) / `run(sql, values)` |
| `{ contains: 'x' }` | `{ $includes: 'x' }`, or `$iincludes` for `mode: 'insensitive'` |
| `{ startsWith: 'x' }` | `{ $startsWith: 'x' }` / `{ $istartsWith: 'x' }` |
| `{ notIn: [...] }` | `{ $nin: [...] }` |
| `{ field: null }` | `{ $isNull: true }` |
| `AND` / `OR` / `NOT` | [`$and` / `$or` / `$not`](https://uql-orm.dev/querying/logical-operators.md) |

Drizzle:

| Drizzle | UQL |
| - | - |
| `db.select().from(users).where(...)` | `findMany(User, { $where })` |
| `db.select({ id: users.id })` | `$select: { id: true }` |
| `db.query.users.findMany({ with: { posts: true } })` | `$populate: { posts: true }` |
| `.orderBy(desc(users.createdAt))` | `$sort: { createdAt: 'desc' }` |
| `.limit(n)` / `.offset(n)` | `$limit: n` / `$skip: n` |
| `db.$count(users, ...)` | `count(User, { $where })` |
| `.groupBy().having()` | [`aggregate(User, { $group, $select, $having })`](https://uql-orm.dev/querying/aggregate.md) |
| `db.insert(users).values(v).returning()` | `insertOne(User, v)` / `insertMany(User, [v])` |
| `db.update(users).set(v).where(...)` | `updateMany(User, { $where }, v)` |
| `db.delete(users).where(...)` | `deleteMany(User, { $where })` |
| `.onConflictDoUpdate({ target, set })` | `upsertOne(User, conflictPaths, data)` |
| `db.transaction(fn)` | [`pool.transaction(fn)`](https://uql-orm.dev/querying/transactions.md) |
| ``db.execute(sql`...`)`` | [`all(sql, values)`](https://uql-orm.dev/querying/raw-sql.md) / `run(sql, values)` |
| `and(...)` / `or(...)` / `not(...)` | [`$and` / `$or` / `$not`](https://uql-orm.dev/querying/logical-operators.md); `$and` is implicit between keys |
| `gte(users.age, 18)` | `{ age: { $gte: 18 } }` |
| `like` / `ilike` | `{ $like }` / `{ $ilike }`, or `$includes` / `$iincludes` to skip the wildcards |
| `inArray` / `notInArray` | `{ $in }` / `{ $nin }` |
| `isNull` / `isNotNull` | `{ $isNull: true }` / `{ $isNotNull: true }` |

TypeORM:

| TypeORM | UQL |
| - | - |
| `find(User, { where, select, order, take, skip })` | `findMany(User, { $where, $select, $sort, $limit, $skip })` |
| `findOne` / `findOneBy` | `findOne(User, { $where })` |
| `findOneBy({ id })` | `findOneById(User, id)` |
| `count` / `countBy` | `count(User, { $where })` |
| `relations: ['posts']`, `leftJoinAndSelect` | [`$populate`](https://uql-orm.dev/querying/relations.md), with `$required: true` for an inner join |
| `createQueryBuilder().groupBy().having()` | [`aggregate(User, { $group, $select, $having })`](https://uql-orm.dev/querying/aggregate.md) |
| `insert(User, data)` | `insertOne(User, data)` / `insertMany(User, data)` |
| `save(entity)` | `saveOne(User, data)`, upserts when the key is set |
| `update(User, id, data)` | `updateOneById(User, id, data)` |
| `delete(User, id)` | `deleteOneById(User, id, { hardDelete: true })` |
| `softDelete` / `restore` | `deleteOneById` / [`restoreOneById`](https://uql-orm.dev/entities/soft-delete.md) |
| `manager.transaction(fn)` | [`pool.transaction(fn)`](https://uql-orm.dev/querying/transactions.md) |
| `manager.query(sql)` | [`all(sql, values)`](https://uql-orm.dev/querying/raw-sql.md) / `run(sql, values)` |
| `MoreThanOrEqual(18)` | `{ $gte: 18 }` |
| `Between(a, b)` | `{ $between: [a, b] }` |
| `Like('%x%')` / `ILike('%x%')` | `{ $includes: 'x' }` / `{ $iincludes: 'x' }` |
| `In([...])` / `Not(In([...]))` | `{ $in: [...] }` / `{ $nin: [...] }` |
| `IsNull()` | `{ $isNull: true }` |

MikroORM:

| MikroORM | UQL |
| - | - |
| `em.find(User, where, { fields, orderBy, limit, offset })` | `findMany(User, { $where, $select, $sort, $limit, $skip })` |
| `em.findOne(User, where)` | `findOne(User, { $where })` / `findOneById(User, id)` |
| `em.count(User, where)` | `count(User, { $where })` |
| `populate` + `populateFilter` | [`$populate`](https://uql-orm.dev/querying/relations.md) with a `$where` per relation |
| `qb.groupBy().having()` | [`aggregate(User, { $group, $select, $having })`](https://uql-orm.dev/querying/aggregate.md) |
| `em.create(...)` + `em.flush()` | `insertOne(User, data)`; there is no flush |
| `em.nativeUpdate(User, where, data)` | `updateMany(User, { $where }, data)` / `updateOneById` |
| `em.nativeDelete(User, where)` | `deleteMany(User, { $where })` / `deleteOneById` |
| `em.upsert` / `em.upsertMany` | `upsertOne` / `upsertMany` |
| `em.transactional(fn)` | [`pool.transaction(fn)`](https://uql-orm.dev/querying/transactions.md) |
| `em.getConnection().execute(sql)` | [`all(sql, values)`](https://uql-orm.dev/querying/raw-sql.md) / `run(sql, values)` |
| `$gte`, `$nin`, `$like`, `$or`, `$elemMatch` | same names, typed per field |
| `$ilike` (PostgreSQL only) | `$ilike`, `$istartsWith`, `$iincludes` on every dialect |
| `filters: { softDelete: ... }` | [`@Field({ softDelete: true })`](https://uql-orm.dev/entities/soft-delete.md), plus general [query filters](https://uql-orm.dev/querying/filters.md) |

Mongoose:

| Mongoose | UQL |
| - | - |
| `User.find(filter).sort().limit().skip()` | `findMany(User, { $where, $sort, $limit, $skip })` |
| `User.findOne(filter)` | `findOne(User, { $where })` |
| `User.findById(id)` | `findOneById(User, id)` |
| `User.countDocuments(filter)` | `count(User, { $where })` |
| `.select('id name')` | `$select: { id: true, name: true }` |
| `.populate('posts')` | [`$populate: { posts: true }`](https://uql-orm.dev/querying/relations.md), as a join |
| `User.aggregate([...])` | [`aggregate(User, { $group, $select, $having })`](https://uql-orm.dev/querying/aggregate.md) |
| `User.create(doc)` / `insertMany` | `insertOne(User, doc)` / `insertMany(User, docs)` |
| `findByIdAndUpdate(id, doc)` | `updateOneById(User, id, doc)` |
| `updateMany(filter, { $set: doc })` | `updateMany(User, { $where }, doc)` |
| `deleteOne` / `deleteMany` | `deleteOneById` / `deleteMany` |
| `session.withTransaction(fn)` | [`pool.transaction(fn)`](https://uql-orm.dev/querying/transactions.md) |
| `{ $regex: 'x' }` | `{ $iincludes: 'x' }` for a plain substring, `$regex` when you need the pattern |
| `{ $gte }`, `{ $in }`, `{ $nin }`, `$or`, `$elemMatch`, `$size` | same names |
| Subdocuments and arrays of objects | a [JSON column](https://uql-orm.dev/querying/json.md), or a relation if you filter or join on it |

### Patterns worth seeing side by side

Where it takes more than renaming keys. The [comparison page](https://uql-orm.dev/comparison.md) covers every common operation.

#### Filtering, sorting, paging

Prisma:

```ts
prisma.user.findMany({
  where: {
    age: { gte: 18 },
    status: 'active',
    email: { contains: '@uql-orm.dev' }
  },
  orderBy: { createdAt: 'desc' },
  take: 10
});
```

Drizzle:

```ts
import { and, gte, eq, like, desc } from 'drizzle-orm';

db.select()
  .from(users)
  .where(and(
    gte(users.age, 18),
    eq(users.status, 'active'),
    like(users.email, '%@uql-orm.dev%')
  ))
  .orderBy(desc(users.createdAt))
  .limit(10);
```

TypeORM:

```ts
import { MoreThanOrEqual, Like } from 'typeorm';

manager.find(User, {
  where: {
    age: MoreThanOrEqual(18),
    status: 'active',
    email: Like('%@uql-orm.dev%')
  },
  order: { createdAt: 'DESC' },
  take: 10
});
```

MikroORM:

```ts
em.find(User, {
  age: { $gte: 18 },
  status: 'active',
  email: { $like: '%@uql-orm.dev%' }
}, {
  orderBy: { createdAt: 'DESC' },
  limit: 10
});
```

Mongoose:

```ts
User.find({
  age: { $gte: 18 },
  status: 'active',
  email: { $regex: '@uql-orm.dev' }
})
  .sort({ createdAt: -1 })
  .limit(10);
```

UQL:

```ts
pool.findMany(User, {
  $where: {
    age: { $gte: 18 },
    status: 'active',
    email: { $includes: '@uql-orm.dev' }
  },
  $sort: { createdAt: 'desc' },
  $limit: 10
});
```

#### Atomic JSON updates

Change one key of a JSON column without rewriting the object:

Other ORMs (raw SQL):

```ts
await db.execute(
  `UPDATE users SET settings = jsonb_set(settings, '{theme}', '"dark"') WHERE id = 1`
);
```

UQL:

```ts
await pool.updateOneById(User, 1, {
  settings: { $set: { theme: 'dark' } }
});
```

Read-modify-write loses concurrent writes to other keys. `$set`, `$unset`, `$push` and `$pull` work on every dialect ([JSON / JSONB](https://uql-orm.dev/querying/json.md)).

Two more differ. In [aggregate queries](https://uql-orm.dev/querying/aggregate.md), grouped columns go in `$group`, computed ones are named in `$select`, and `$having` and `$sort` are checked against those names. Semantic search is a `$sort` with `$vector`, one typed query on every engine with vectors ([AI & RAG](https://uql-orm.dev/ai-semantic-search.md)).

## Migrating in phases

Each phase keeps the old path until the new one is proven.

### Phase 1: reads, on one endpoint

Rewrite one non-critical read with UQL and keep the old one. Run both and log any differences. The worst case is a bad response on one endpoint.

### Phase 2: new tables and features

Build everything new on UQL, with [`uql-migrate generate:entities`](https://uql-orm.dev/migrations.md#from-entities-to-the-database) creating the tables. You try the whole loop on data nothing else depends on.

### Phase 3: writes, one entity at a time

Move writes per entity, not per endpoint, so each table has one writer at a time. Keep the old write path until the entity is verified in production. Port the old ORM’s callbacks, cascades and validation to [lifecycle hooks](https://uql-orm.dev/entities/lifecycle-hooks.md) in the same change; nothing warns you if they go missing.

### Phase 4: cutover

When nothing uses the old ORM, remove it with its `.prisma` file, Drizzle snapshots or data source config. Keep its migration history table if you may need to audit it. Your `@Entity` classes are then the only schema.

## Habits to unlearn

Beyond the [shift in mental model](#the-shift-in-mental-model):

- **`require()`.** UQL is ESM only. Use `import`, and add `"type": "module"` to `package.json` if Node runs your compiled output. See [Requirements](https://uql-orm.dev/getting-started.md#requirements) for the `tsconfig.json` settings.
