Cardinal

Static database linter · TypeScript & JavaScript

Spot the query your ORM
quietly runs 10,000 times.

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 →
users.service.ts
 const users = await prisma.user.findMany()
 for (const user of users) {
   await prisma.post.findMany({ where: { authorId: user.id } })
 }
error n-plus-one Query on “post” runs once per user - batch it into one query.
warning unindexed-query No index covers “authorId” - add @@index([authorId]).

What it catches

One innocent line. One melted database.

error

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.

warning

Queries no index covers

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.

warning

Unbounded reads

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.

warning

Over-fetching

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.

warning

Slow SQL patterns

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.

Database-aware

Not a style linter - it reads query shape, index coverage, and the real cost of a round trip.

100% static

No LLM, no network, no database connection. Cardinal never sees your data - only your code, on your machine.

Why Cardinal

You could ask an AI to check every query.
You won't - not every time.

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.

Ask an AI to review
When
Only when you remember to
Cost
Non-deterministic, burns tokens
Your data
Guesses your scale
Watch prod dashboards
When
After users already feel it
Cost
A postmortem
Your data
Too late to matter
Cardinal You
When
Every keystroke, automatically
Cost
Deterministic, free, offline
Your data
Reads your schema's indexes + your real row counts

How it works

Reads your queries the way the database will.

Adapters
  • Prisma
  • Drizzle
  • Mongoose
  • TypeORM
  • raw SQL
  • + heuristic fallback

It already knows your indexes

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])

Tell it what code can't say

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

Every finding ships its fix

Not just “this is slow” - the batched query that replaces the loop, ready to paste. AI agents get the same fix as JSON.

Before
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

Early, and moving.

Tuned by real codebases: every wrong finding you report becomes a permanent test. Fixed once, fixed forever.

  • shipped Schema-awareness for Prisma: unindexed-query reads @id / @unique / @@index straight from schema.prisma
  • shipped Config file, a real SQL parser, and the VS Code Marketplace + Open VSX
  • shipped Context-aware over-fetch & cardinality across Drizzle, Mongoose, TypeORM, and raw SQL
  • shipped TypeORM adapter and machine-readable --format json output for AI agents
  • next Index extraction for Drizzle, TypeORM, and Mongoose - unindexed-query beyond Prisma
  • next More parser-backed SQL rules: subqueries, HAVING / GROUP BY misuse, SELECT *
  • later More engines (MySQL, Postgres, PlanetScale) and layers (Kysely, Sequelize)
  • explore Deep mode: cross-module data-flow checks