> 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

# What survives a rename? Six TypeScript ORMs

> Six ORMs renamed the way your editor renames, and compiled live: which mentions follow, which the compiler catches, and which break silently.

Source: https://uql-orm.dev/rename-safety

A rename is a refactor nobody thinks twice about because it is often pretty simple: just press F2, enter the new name and trust that every place is updated. An ORM’s rename reaches only as far as it links its schema to the code.

Each tab below is one ORM’s model and 19 places that name three of its members, renamed the way F2 renames: `emailAddress` to `email`, `employerId` to `workplaceId`, `employer` to `workplace`. Green is what the rename edited, red what it left behind for the compiler to stop, and amber what it left behind that still compiles.

- renamed
- stopped by the compiler
- left behind silently

```ts
import { count, eq, relations, type SQL, sql } from 'drizzle-orm';
import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
import {
  check,
  index,
  integer,
  pgTable,
  serial,
  text,
  uniqueIndex,
} from 'drizzle-orm/pg-core';

export const companies = pgTable('Company', { id: serial().primaryKey() });

export const users = pgTable(
  'User',
  {
    id: serial().primaryKey(),
    emailAddress: text().notNull(),
    employerId: integer().references(() => companies.id),
    // Generated column
    emailLower: text().generatedAlwaysAs(
      (): SQL => sql`lower(${users.emailAddress})`,
    ),
  },
  (t) => [
    // Index on the field
    index().on(t.emailAddress),
    // Composite unique index
    uniqueIndex().on(t.emailAddress, t.employerId),
    // Covering index column | n/a: Drizzle's index builder has no INCLUDE
    // Expression index
    index().on(sql`lower(${t.emailAddress})`),
    // Partial index condition
    index()
      .on(t.employerId)
      .where(sql`${t.emailAddress} <> ''`),
    // Check constraint
    check('address_present', sql`${t.emailAddress} <> ''`),
  ],
);

export const usersRelations = relations(users, ({ one }) => ({
  // Foreign key of a relation
  employer: one(companies, {
    fields: [users.employerId],
    references: [companies.id],
  }),
}));

export const companiesRelations = relations(companies, ({ many }) => ({
  // Inverse side of a relation
  staff: many(users),
}));

const schema = { companies, users, usersRelations, companiesRelations };
declare const db: NodePgDatabase<typeof schema>;

// Field in the projection
await db.select({ id: users.id, address: users.emailAddress }).from(users);

// Field in the filter
await db.select().from(users).where(eq(users.emailAddress, 'ada@example.com'));

// Field in the sort
await db.select().from(users).orderBy(users.emailAddress);

// Field inside a loaded relation
await db.query.companies.findMany({
  with: { staff: { columns: { emailAddress: true } } },
});

// Relation loaded by name
await db.query.users.findMany({ with: { employer: true } });

// Field in inserted data
await db.insert(users).values({ emailAddress: 'ada@example.com' });

// Field in updated data
await db
  .update(users)
  .set({ emailAddress: 'ada@example.com' })
  .where(eq(users.id, 1));

// Foreign key in a grouped count
await db
  .select({ company: users.employerId, total: count() })
  .from(users)
  .groupBy(users.employerId);

// Field read off the result
const [user] = await db.select().from(users);
export const address = user.emailAddress;

// Raw SQL in a filter
await db
  .select()
  .from(users)
  .where(sql`lower(${users.emailAddress}) = ${'ada@example.com'}`);
```

rename-safety/drizzle.ts · static preview

UQL follows all 19: its indexes, checks and relations name members through callbacks, and every query key is typed off the entity. Drizzle leaves none behind silently, but 4 for the compiler to stop, where its writes and loaded relations are checked against the table without being linked to it. The other four leave some behind with no error at all: 12 in Sequelize, whose indexes, associations and query options name members as plain strings, 5 in TypeORM, 3 in MikroORM and 3 in Prisma, each breaking only when the code runs or the schema migrates.

## Where this comes from

Every file, rename and mark is [ts-orm-benchmark](https://github.com/rogerpadilla/ts-orm-benchmark#rename-safety)’s. Each rename is the one the ORM’s own language server made there, TypeScript 7.0.2’s or, for Prisma’s schema, prisma-language-server 31.12.10’s, replayed here in your browser, where the compiler this site builds with answers it live. The build fails if F2 in this editor would edit any other place, or if a red line would move.

Prisma renames its schema and never touches its queries, so its tab renames `schema.prisma`, regenerates the client, and shows the queries compiled against it, as the benchmark does. TypeORM’s decorators are the legacy kind, so the benchmark compiles its file in a project of its own and the editor checks it in a worker of its own.

I wrote UQL, so take this for what it is worth from its author: the benchmark is in the open, and every tab above runs the same compiler.

[Type Safety](https://uql-orm.dev/type-safety.md)Six ORMs against ordinary mistakes: a misspelled key, a text operator on a number, a column you did not select.

[Benchmark](https://uql-orm.dev/benchmark.md)What each ORM costs on a full PostgreSQL round trip, on Bun, Node and Deno.
