Running the same SQL checks in a browser, CLI and pull request
I wanted one set of SQL checks to work in three places: while exploring a query, from a terminal and during code review. That became SQL Atlas. It is a local, deterministic SQL analyzer with a browser interface, a CLI and a GitHub Action. This article covers the interfaces, the CI contract and the limits of static SQL analysis.
One analyzer, three interfaces
The analyzer returns structured data instead of printing messages directly. Each interface decides how to present the same result:
- The browser explains findings and links them to learning material.
- The CLI returns text, JSON or Markdown and uses stable exit codes.
- The GitHub Action converts findings into file annotations and a job summary.
Keeping presentation outside the analyzer prevents the CLI and Action from becoming separate implementations with different behavior.
A CLI needs a contract
The CLI accepts one or more files, or SQL through standard input:
npx --yes sql-atlas@0.5.1 analyze query.sql
echo "SELECT * FROM customers;" | npx --yes sql-atlas@0.5.1 analyze -
It supports PostgreSQL, MySQL, Oracle, SQLite, SQL Server and a generic mode. Output can be text for a person, JSON for another program or Markdown for an issue or report. Exit codes are part of the interface:
- 0 means analysis completed and the configured policy passed.
- 1 means analysis completed but a severity or score threshold failed.
- 2 means the command or input was invalid.
This distinction matters in CI. A policy failure is not the same as a broken invocation.
Turning findings into pull request feedback
The Action runs as a bundled Node 24 program and does not download dependencies at runtime. A minimal workflow looks like this:
name: SQL review
on:
pull_request:
paths:
- "**/*.sql"
permissions:
contents: read
jobs:
sql-atlas:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: milekv/sql-atlas@v0.5.1
with:
paths: |
migrations/**/*.sql
schema/**/*.sql
dialect: postgresql
fail-on: critical
min-score: 60
Findings become GitHub file annotations. The full result is written to the job summary, and the Action exposes file count, finding count and lowest score as outputs. The default policy only fails on critical findings. Teams can start in report-only mode with fail-on: none, inspect false positives and add stricter thresholds later.
What static analysis cannot know
SQL Atlas does not connect to a database. It cannot know table sizes, data distribution, available indexes, planner settings or the real execution plan. For that reason, a warning such as a function applied to a filtered column means "check whether this blocks the index strategy you expect", not "this query is slow". Runtime performance still needs EXPLAIN, representative data and production-like measurements. The browser includes a local PostgreSQL EXPLAIN JSON viewer for that next step, but the analyzer deliberately keeps its claims narrow.
Testing the distribution surfaces
The project tests the analyzer and both automation interfaces. CI builds the web app, CLI and Action bundle. A smoke workflow runs the repository's own Action against a known SQL file and verifies its outputs. CI also rebuilds the committed Action bundle and checks that it has no uncommitted difference. The CLI package has no runtime dependencies. I verified the public npm package from an empty directory with a clean cache, including the executable version and a real stdin analysis.
Try it
- Demo: https://milekv.github.io/sql-atlas/
- Source: https://github.com/milekv/sql-atlas
- npm: https://www.npmjs.com/package/sql-atlas
- v0.5.1: https://github.com/milekv/sql-atlas/releases/tag/v0.5.1
I am particularly interested in examples where a rule is too broad, misses a dialect detail or produces an unhelpful CI annotation.
Comments
No comments yet. Start the discussion.