Reference

ORM Detection Patterns

How Technical Debt Radar detects query patterns, N+1 issues, pagination, and raw SQL across 7 supported ORMs.

ORM Detection Patterns

Radar's performance analyzer recognizes query patterns from 7 ORMs. For each ORM, it detects unbounded queries, N+1 patterns, missing pagination, raw SQL without limits, and count operations. Detection uses AST pattern matching via ts-morph --- it reads the code structure, not runtime behavior.


Prisma

Find/FindAll Detection

Radar identifies findMany, findFirst, findUnique, findUniqueOrThrow, and findFirstOrThrow calls on any Prisma model.

// Detected as unbounded query
const users = await prisma.user.findMany();

// Not flagged (has take)
const users = await prisma.user.findMany({ take: 50 });

// Not flagged (has where + take)
const users = await prisma.user.findMany({
  where: { active: true },
  take: 50,
  skip: page * 50,
});

N+1 Detection

Flagged when findMany/findFirst appears inside a for/for...of/forEach loop or inside a .map() callback.

// Detected as N+1
for (const order of orders) {
  const items = await prisma.orderItem.findMany({
    where: { orderId: order.id },
  });
}

// Fix: use include
const orders = await prisma.order.findMany({
  include: { items: true },
});

Pagination Check

Radar checks for take and skip properties in the query options object. Cursor-based pagination (cursor) is also recognized.

Raw SQL Detection

$queryRaw, $executeRaw, $queryRawUnsafe, and $executeRawUnsafe are analyzed for LIMIT clauses in the template string.

// Flagged: raw SQL without LIMIT
await prisma.$queryRaw`SELECT * FROM events WHERE type = ${type}`;

// Not flagged
await prisma.$queryRaw`SELECT * FROM events WHERE type = ${type} LIMIT 100`;

Count Detection

prisma.model.count() without a where clause on large tables is flagged.


TypeORM

Find/FindAll Detection

Radar detects .find(), .findAndCount(), .findOne(), .findOneBy(), and query builder patterns with .getMany() / .getOne().

// Detected as unbounded
const users = await userRepo.find();

// Not flagged (has take)
const users = await userRepo.find({ take: 50, skip: 0 });

// Query builder - detected as unbounded
const users = await userRepo.createQueryBuilder('user').getMany();

// Query builder - not flagged
const users = await userRepo.createQueryBuilder('user')
  .take(50)
  .skip(0)
  .getMany();

N+1 Detection

.find() or .findOne() calls inside loops. Also detects lazy-loaded relations accessed in loops.

// Detected as N+1
for (const order of orders) {
  const customer = await customerRepo.findOneBy({ id: order.customerId });
}

// Fix: use relations or join
const orders = await orderRepo.find({
  relations: ['customer'],
});

Pagination Check

Checks for take and skip in FindManyOptions or .take() / .skip() on query builders.

Raw SQL Detection

.query() and .createQueryRunner().query() are analyzed for LIMIT.

// Flagged
await dataSource.query('SELECT * FROM events');

// Not flagged
await dataSource.query('SELECT * FROM events LIMIT 100');

Count Detection

.count() without conditions is flagged on large tables.


Sequelize

Find/FindAll Detection

Radar detects .findAll(), .findOne(), .findByPk(), .findAndCountAll(), and .findOrCreate().

// Detected as unbounded
const orders = await Order.findAll();

// Not flagged (has limit)
const orders = await Order.findAll({ limit: 50, offset: 0 });

// Not flagged (has where + limit)
const orders = await Order.findAll({
  where: { status: 'active' },
  limit: 50,
});

N+1 Detection

.findAll() or .findOne() inside loops.

// Detected as N+1
for (const order of orders) {
  const items = await OrderItem.findAll({ where: { orderId: order.id } });
}

// Fix: use include (eager loading)
const orders = await Order.findAll({
  include: [{ model: OrderItem }],
});

Pagination Check

Checks for limit and offset in query options.

Raw SQL Detection

sequelize.query() is analyzed for LIMIT.

Count Detection

Model.count() without a where option is flagged.


Mongoose

Find/FindAll Detection

Radar detects .find(), .findOne(), .findById(), and aggregate pipelines.

// Detected as unbounded
const users = await User.find();

// Not flagged (has limit)
const users = await User.find().limit(50).skip(0);

// Not flagged (has query + limit)
const users = await User.find({ active: true }).limit(50);

N+1 Detection

.find() or .findById() inside loops.

// Detected as N+1
for (const order of orders) {
  const customer = await Customer.findById(order.customerId);
}

// Fix: use populate or batch query
const orders = await Order.find().populate('customer');
// Or: batch with $in
const customerIds = orders.map(o => o.customerId);
const customers = await Customer.find({ _id: { $in: customerIds } });

Pagination Check

Checks for .limit() chain call or $limit in aggregate pipelines.

Raw SQL Detection

Not applicable for Mongoose (MongoDB). Aggregate pipelines without $limit are flagged instead.

// Flagged: aggregate without $limit
const results = await Order.aggregate([
  { $match: { status: 'completed' } },
  { $group: { _id: '$customerId', total: { $sum: '$amount' } } },
]);

// Not flagged
const results = await Order.aggregate([
  { $match: { status: 'completed' } },
  { $group: { _id: '$customerId', total: { $sum: '$amount' } } },
  { $limit: 100 },
]);

Count Detection

Model.countDocuments() or Model.estimatedDocumentCount() without filters.


Drizzle

Find/FindAll Detection

Radar detects db.select().from(), db.query.table.findMany(), and db.query.table.findFirst().

// Detected as unbounded
const users = await db.select().from(users);

// Not flagged (has limit)
const users = await db.select().from(users).limit(50).offset(0);

// Query API - detected as unbounded
const result = await db.query.users.findMany();

// Query API - not flagged
const result = await db.query.users.findMany({ limit: 50 });

N+1 Detection

Select or query calls inside loops.

Pagination Check

Checks for .limit() chain call or limit property in query options.

Raw SQL Detection

sql tagged templates and db.execute() analyzed for LIMIT.

// Flagged
await db.execute(sql`SELECT * FROM events`);

// Not flagged
await db.execute(sql`SELECT * FROM events LIMIT 100`);

Count Detection

db.select({ count: count() }).from(table) without .where().


Knex

Find/FindAll Detection

Radar detects knex('table'), knex.select().from(), and chained query patterns.

// Detected as unbounded
const users = await knex('users');

// Not flagged (has limit)
const users = await knex('users').limit(50).offset(0);

// Not flagged (has where + limit)
const users = await knex('users').where({ active: true }).limit(50);

N+1 Detection

Knex queries inside loops.

Pagination Check

Checks for .limit() and .offset() chain calls.

Raw SQL Detection

knex.raw() analyzed for LIMIT.

// Flagged
await knex.raw('SELECT * FROM events WHERE type = ?', [type]);

// Not flagged
await knex.raw('SELECT * FROM events WHERE type = ? LIMIT 100', [type]);

Count Detection

knex('table').count() without .where().


MikroORM

Find/FindAll Detection

Radar detects em.find(), em.findAll(), em.findOne(), em.findOneOrFail(), and repository variants.

// Detected as unbounded
const users = await em.find(User, {});

// Not flagged (has limit)
const users = await em.find(User, {}, { limit: 50, offset: 0 });

// Repository variant - detected as unbounded
const users = await userRepo.findAll();

// Repository variant - not flagged
const users = await userRepo.findAll({ limit: 50 });

N+1 Detection

em.find() or em.findOne() calls inside loops.

// Detected as N+1
for (const order of orders) {
  const customer = await em.findOne(Customer, { id: order.customerId });
}

// Fix: use populate
const orders = await em.find(Order, {}, { populate: ['customer'] });

Pagination Check

Checks for limit and offset in the options parameter.

Raw SQL Detection

em.execute() and em.getConnection().execute() analyzed for LIMIT.

Count Detection

em.count(Entity, {}) with an empty filter object on large tables.


Detection Summary

ORMFind/FindAllN+1PaginationRaw SQLCount
PrismafindMany, findFirstLoop + findtake/skip/cursor$queryRaw.count()
TypeORM.find(), .getMany()Loop + findtake/skip.query().count()
Sequelize.findAll()Loop + findAlllimit/offset.query().count()
Mongoose.find()Loop + find.limit()Aggregate $limit.countDocuments()
Drizzleselect().from()Loop + select.limit()sql / executecount()
Knexknex('table')Loop + knex.limit().raw().count()
MikroORMem.find()Loop + findlimit/offsetem.execute()em.count()
Technical Debt Radar Documentation