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
| ORM | Find/FindAll | N+1 | Pagination | Raw SQL | Count |
|---|---|---|---|---|---|
| Prisma | findMany, findFirst | Loop + find | take/skip/cursor | $queryRaw | .count() |
| TypeORM | .find(), .getMany() | Loop + find | take/skip | .query() | .count() |
| Sequelize | .findAll() | Loop + findAll | limit/offset | .query() | .count() |
| Mongoose | .find() | Loop + find | .limit() | Aggregate $limit | .countDocuments() |
| Drizzle | select().from() | Loop + select | .limit() | sql / execute | count() |
| Knex | knex('table') | Loop + knex | .limit() | .raw() | .count() |
| MikroORM | em.find() | Loop + find | limit/offset | em.execute() | em.count() |