Switching to UQL
Moving to a new ORM is a significant architectural decision. This guide is designed to help you translate your existing mental models and patterns into the UQL way of doing things, focusing on reducing friction, eliminating “ORM taxes,” and avoiding common migration pitfalls.
The Mental Model Shift
Section titled “The Mental Model Shift”Most ORMs impose a “tax” on the developer—either in the form of build-time complexity, runtime verbosity, or state-management overhead. UQL is designed to eliminate these taxes.
⚡️ The “Generation Tax” (From Prisma)
Section titled “⚡️ The “Generation Tax” (From Prisma)”The Shift: From Proprietary DSL $\rightarrow$ Pure TypeScript.
Prisma requires a .prisma file and a generate step. This creates a disconnect between your code and your schema, and can slow down CI/CD pipelines.
- Prisma: Edit
.prisma$\rightarrow$npx prisma generate$\rightarrow$ Use Client. - UQL: Edit
@Entityclass $\rightarrow$ Use Querier. Your code is the schema.
⚡️ The “Verbosity Tax” (From Drizzle)
Section titled “⚡️ The “Verbosity Tax” (From Drizzle)”The Shift: From SQL-Construction $\rightarrow$ Declarative Data.
Drizzle is an excellent thin wrapper, but complex queries often become a forest of eq(), and(), and sql templates. UQL uses a JSON-native declarative API that is easier to read, write, and—crucially—transport over the network.
- Drizzle:
db.select().from(users).where(and(eq(users.id, 1), gte(users.age, 18))) - UQL:
querier.findMany(User, { $where: { id: 1, age: { $gte: 18 } } })
⚡️ The “State Tax” (From TypeORM / MikroORM)
Section titled “⚡️ The “State Tax” (From TypeORM / MikroORM)”The Shift: From Managed Entities $\rightarrow$ Lean Results. Heavy ORMs use an “Identity Map” or “Unit of Work.” While powerful, this often leads to the dreaded “Detached Entity” error or unexpected database updates when you simply modified a property on an object.
- Heavy ORMs:
user.name = 'New Name'; await em.flush();(Implicit state tracking). - UQL:
await querier.updateOneById(User, id, { name: 'New Name' });(Explicit, predictable mutations).
Pattern Translation: Before & After
Section titled “Pattern Translation: Before & After”The following examples demonstrate how to translate common “industry standard” patterns into UQL.
1. Complex Filtering & Search
Section titled “1. Complex Filtering & Search”How to handle multiple conditions and range filters.
// Prismaprisma.user.findMany({ where: { age: { gte: 18 }, status: 'active', email: { contains: '@uql-orm.dev' } }});
// Drizzledb.select().from(users).where( and(gte(users.age, 18), eq(users.status, 'active'), like(users.email, '%@uql-orm.dev%')));querier.findMany(User, { $where: { age: { $gte: 18 }, status: 'active', email: { $contains: '@uql-orm.dev' } }});2. Deeply Nested Relations
Section titled “2. Deeply Nested Relations”Fetching a user, their posts, and the comments on those posts.
// Prismaprisma.user.findMany({ include: { posts: { include: { comments: true } } }});
// Drizzle (Relational API)db.query.users.findMany({ with: { posts: { with: { comments: true } } }});querier.findMany(User, { $select: { posts: { $select: { comments: true } } }});3. JSONB Power-Moves
Section titled “3. JSONB Power-Moves”Updating a specific key inside a JSONB column without overwriting the whole object.
// Most ORMs require raw SQL for atomic JSON updatesawait db.execute( `UPDATE users SET settings = jsonb_set(settings, '{theme}', '"dark"') WHERE id = 1`);// Atomic JSON update via the $set operatorawait querier.updateOneById(User, 1, { settings: { $set: { theme: 'dark' } }});Migration Strategy: The “Zero-Downtime” Path
Section titled “Migration Strategy: The “Zero-Downtime” Path”We strongly discourage a “Big Bang” rewrite. Instead, use this phased approach to migrate a production system with confidence.
Phase 1: The Read-Only Shadow (Low Risk)
Section titled “Phase 1: The Read-Only Shadow (Low Risk)”Introduce UQL purely for Read operations.
- Strategy: Pick a non-critical endpoint and implement it with UQL.
- Verification: Run both queries (Legacy and UQL) in parallel and log any discrepancies in the result sets.
- Why: UQL returns plain objects, so it can coexist with any other ORM without interfering with their internal caches or state.
Phase 2: The “New Feature” Pilot (Medium Risk)
Section titled “Phase 2: The “New Feature” Pilot (Medium Risk)”Implement all new tables and features using UQL.
- Strategy: Use
uql-migrateto create new tables. - Benefit: You get to test the full UQL lifecycle (Definition $\rightarrow$ Migration $\rightarrow$ Query) on fresh data before touching legacy tables.
Phase 3: The Mutation Flip (High Risk)
Section titled “Phase 3: The Mutation Flip (High Risk)”Migrate INSERT, UPDATE, and DELETE operations.
- Strategy: Move mutations one entity at a time.
- Tip: This is where you will first notice the performance gains of the zero-allocation engine and the simplicity of the atomic JSON operators.
Phase 4: The Total Cutover
Section titled “Phase 4: The Total Cutover”Once the legacy ORM is no longer being used for any queries, remove it from your package.json.
- Final Step: Delete the
.prismafiles, the Drizzle snapshots, or the complex TypeORM config. Your@Entityclasses are now your only source of truth.
Common Pitfalls & “Muscle Memory” Warnings
Section titled “Common Pitfalls & “Muscle Memory” Warnings”The “Migration Mindset” Shift
Coming from a different ORM creates “muscle memory” that can lead to bugs. Be mindful of these:
- The “Identity Map” Reflex: If you are coming from MikroORM, remember: UQL does not track state. Changing
user.name = 'Bob'does nothing to the database. You must explicitly callupdateOne. - The “DSL” Reflex: If you are coming from Prisma, you might look for a schema file to check a relation. Stop. Look at the
@Entityclass. If it’s not in the TS class, it’s not in the DB. - The “CommonJS” Trap: UQL is Pure ESM. If your project is still using
require(), you will need to migrate toimportand set"type": "module"in yourpackage.json. - The “Generation” Habit: You no longer need to run a “generate” command after changing a field. Just save the file and run your migration.
Summary: When should you actually switch?
Section titled “Summary: When should you actually switch?”| If you are tired of… | …then UQL is your solution |
|---|---|
Slow npx prisma generate steps | Pure TS entities $\rightarrow$ No codegen |
Writing sql templates for everything | Declarative JSON-native API |
| ”Detached Entity” or “Unit of Work” bugs | Lean, plain-object results |
| Manual JSONB manipulation via raw SQL | First-class atomic JSON operators |
| Complex build-steps for migrations | Direct Code $\rightarrow$ DB diffing |