N+1 loops
n-plus-one A query inside a loop runs once per row: 100 rows is 101 round trips, and 10,000 rows is an incident. Cardinal points at the single batched query that replaces the loop - and stays quiet when it can prove the loop is small.
Static database linter · TypeScript & JavaScript
Cardinal flags the queries that melt under production data - N+1 loops, full-table scans, filters no index covers - as you type, not in the postmortem. No LLM, no network. Your code never leaves your machine.
Also on Open VSX - Cursor & VSCodium → const users = await prisma.user.findMany()
for (const user of users) {
await prisma.post.findMany({ where: { authorId: user.id } })
} @@index([authorId]). What it catches
n-plus-one A query inside a loop runs once per row: 100 rows is 101 round trips, and 10,000 rows is an incident. Cardinal points at the single batched query that replaces the loop - and stays quiet when it can prove the loop is small.
unindexed-query Cardinal reads the indexes in your schema.prisma - zero setup. Filter or sort on a column no index covers and it hands you the exact @@index([...]) to add, before the table scan finds you in production.
unbounded-read No WHERE, no LIMIT - the whole table comes back. Fast with 200 dev rows, a memory spike with 2 million real ones. Flagged the moment you type it.
over-fetch Loading 10,000 rows to use 10. Tell Cardinal your table sizes once, and it names the narrower read - down to the exact filter that returns the small subset.
raw SQL ORDER BY RAND(), LIKE '%…', six-table joins - the classics that skip every index. Parsed with a real SQL parser, flagged like everything else.
Not a style linter - it reads query shape, index coverage, and the real cost of a round trip.
No LLM, no network, no database connection. Cardinal never sees your data - only your code, on your machine.
Why Cardinal
An AI review happens when you remember to ask. Cardinal is the reviewer that never blinks - and the only one that knows your indexes and how big your tables actually are.
How it works
Cardinal parses your schema.prisma - zero setup. Query by
“email” and it stays quiet; query by “name” and it hands you the
missing index.
model User {
id Int @id
email String @unique
name String
} warning filter on “name” has no index - add @@index([name])
A loop over 12 admin roles is fine; the same loop over 2M users is an
outage. Write your row counts in cardinal.knowledge.yaml
once - Cardinal silences the small loops and escalates the big ones.
version: 1
tables:
user:
rows: 10000
filters:
- when: { status: active }
rows: 10 Not just “this is slow” - the batched query that replaces the loop, ready to paste. AI agents get the same fix as JSON.
for (const u of users)
await prisma.post.findMany(
{ where: { authorId: u.id } }) After await prisma.post.findMany({
where: { authorId: { in: ids } },
}) What's next
Tuned by real codebases: every wrong finding you report becomes a permanent test. Fixed once, fixed forever.