Skip to content

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.

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 @Entity class $\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).

The following examples demonstrate how to translate common “industry standard” patterns into UQL.

How to handle multiple conditions and range filters.

// Prisma
prisma.user.findMany({
where: {
age: { gte: 18 },
status: 'active',
email: { contains: '@uql-orm.dev' }
}
});
// Drizzle
db.select().from(users).where(
and(gte(users.age, 18), eq(users.status, 'active'), like(users.email, '%@uql-orm.dev%'))
);

Fetching a user, their posts, and the comments on those posts.

// Prisma
prisma.user.findMany({
include: {
posts: {
include: { comments: true }
}
}
});
// Drizzle (Relational API)
db.query.users.findMany({
with: {
posts: { with: { comments: true } }
}
});

Updating a specific key inside a JSONB column without overwriting the whole object.

// Most ORMs require raw SQL for atomic JSON updates
await db.execute(
`UPDATE users SET settings = jsonb_set(settings, '{theme}', '"dark"') WHERE id = 1`
);

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.

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-migrate to create new tables.
  • Benefit: You get to test the full UQL lifecycle (Definition $\rightarrow$ Migration $\rightarrow$ Query) on fresh data before touching legacy tables.

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.

Once the legacy ORM is no longer being used for any queries, remove it from your package.json.

  • Final Step: Delete the .prisma files, the Drizzle snapshots, or the complex TypeORM config. Your @Entity classes 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 call updateOne.
  • The “DSL” Reflex: If you are coming from Prisma, you might look for a schema file to check a relation. Stop. Look at the @Entity class. 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 to import and set "type": "module" in your package.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.
If you are tired of……then UQL is your solution
Slow npx prisma generate stepsPure TS entities $\rightarrow$ No codegen
Writing sql templates for everythingDeclarative JSON-native API
”Detached Entity” or “Unit of Work” bugsLean, plain-object results
Manual JSONB manipulation via raw SQLFirst-class atomic JSON operators
Complex build-steps for migrationsDirect Code $\rightarrow$ DB diffing