Fast, rule-based SQL linter. 15 rules. Zero config. Instant results.
Catches dangerous SQL before it reaches production -- DELETE without WHERE, SQL injection patterns, SELECT *, and 12 more. Runs as a CLI tool, pre-commit hook, and GitHub Action.
For deeper AI-powered analysis, pair with SQL Ops Reviewer.
pip install sql-sop
sql-sop check .queries/create_orders.sql
L3: ERROR [E001] DELETE without WHERE clause -- this will delete all rows
-> Add a WHERE clause to limit affected rows
L7: WARN [W001] SELECT * -- specify columns explicitly
-> Replace with: SELECT col1, col2, col3 FROM ...
Found 2 issues (1 error, 1 warning) in 1 file (0.001s)
Most teams have no SQL review process. Some use an AI linter. The problem: AI is slow, expensive, and overkill for catching DELETE FROM users;.
sql-sop and SQL Ops Reviewer solve this together:
┌─────────────────────────────────────┐
│ YOUR SQL FILE │
└──────────────┬──────────────────────┘
│
┌────────────────────────┼────────────────────────┐
│ │ │
▼ │ │
LAYER 1: PRE-COMMIT │ LAYER 2: CI
───────────────── │ ──────────
sql-guard │ SQL Ops Reviewer
│
When: before every commit │ When: on every PR
Speed: <0.2 seconds │ Speed: 10-40 seconds
How: regex pattern matching │ How: Ollama LLM analysis
Needs: nothing (pure Python) │ Needs: 4-7 GB (AI model)
Catches: 80% of issues │ Catches: remaining 20%
│
✓ DELETE without WHERE │ ✓ wrong JOIN type
✓ SELECT * │ ✓ business logic errors
✓ SQL injection patterns │ ✓ schema-aware suggestions
✓ missing LIMIT │ ✓ cross-table consistency
✓ DROP without IF EXISTS │ ✓ performance rewrites
│ │ │
▼ │ ▼
commit blocked or passes │ PR comment with findings
│ │ │
└────────────────────────┼────────────────────────┘
│
▼
CLEAN SQL IN PRODUCTION
Layer 1 (sql-guard) is a smoke detector -- always on, instant, catches fire fast. Layer 2 (SQL Ops Reviewer) is a fire inspector -- thorough, comes on every PR.
You want both.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/Pawansingh3889/sql-guard
rev: v0.1.0
hooks:
- id: sql-guard
args: [--severity, error] # only block on errors locallypip install pre-commit
pre-commit installNow every git commit with .sql changes runs sql-guard automatically. Errors block the commit. Warnings are shown but don't block.
# .github/workflows/sql-quality.yml
name: SQL Quality
on:
pull_request:
paths: ['**/*.sql']
permissions:
contents: read
pull-requests: write
jobs:
# Layer 1: fast rule check (~2 seconds)
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Pawansingh3889/sql-guard@v1
with:
severity: warning
# Layer 2: deep AI review (~30 seconds, runs after lint passes)
review:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Pawansingh3889/sql-ops-reviewer@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}That's it. Two files. Every SQL change gets:
- Instant rule-based lint (sql-guard)
- Deep AI review with fix suggestions (SQL Ops Reviewer)
pip install sql-sop
sql-sop check . # scan current directory
sql-sop check queries/ --severity error # errors only
sql-sop check . --fail-fast # stop on first error
sql-sop check . --disable E002 W008 # skip specific rules
sql-sop list-rules # show all 15 rules| ID | Name | What it catches |
|---|---|---|
| E001 | delete-without-where |
DELETE FROM orders; -- deletes all rows |
| E002 | drop-without-if-exists |
DROP TABLE users; -- fails if table missing |
| E003 | grant-revoke |
GRANT SELECT ON users TO public; -- privilege escalation |
| E004 | string-concat-in-where |
WHERE id = '' + @input -- SQL injection |
| E005 | insert-without-columns |
INSERT INTO t VALUES (...) -- breaks on schema change |
| ID | Name | What it catches |
|---|---|---|
| W001 | select-star |
SELECT * FROM users -- pulls unnecessary columns |
| W002 | missing-limit |
Unbounded SELECT -- could return millions of rows |
| W003 | function-on-column |
WHERE YEAR(date) = 2024 -- kills index usage |
| W004 | missing-alias |
JOIN without table aliases -- hard to read |
| W005 | subquery-in-where |
WHERE x IN (SELECT ...) -- often slower than JOIN |
| W006 | orderby-without-limit |
ORDER BY without LIMIT -- sorts entire result |
| W007 | hardcoded-values |
WHERE amount > 10000 -- use parameters |
| W008 | mixed-case-keywords |
select ... FROM -- inconsistent casing |
| W009 | missing-semicolon |
Statement not terminated with ; |
| W010 | commented-out-code |
-- SELECT * FROM old_table -- use version control |
sql-sop check . --disable E002 W008 W010sql-sop check . --severity error # only show errors
sql-sop check . --severity warning # show everything (default)sql-sop check . --fail-fast # stop after first error foundsql-guard is designed to be fast:
- Compiled regex -- patterns compiled once at startup, reused per file
- Two-pass scanning -- single-line rules run first (10 of 15 rules), multi-line parsing only when needed
- Line-by-line streaming -- files read line by line, not loaded entirely into memory
- Early exit --
--fail-faststops on first error
Benchmark: 200 SQL files, 15 rules
sql-guard: 0.08 seconds
sqlfluff: 45 seconds (560x slower)
| sql-sop | sqlfluff | sql-lint | |
|---|---|---|---|
| Rules | 15 (focused) | 800+ (comprehensive) | ~20 |
| Speed | <0.1s for 200 files | 45s for 200 files | ~2s |
| Config needed | Zero | Extensive | Minimal |
| Language | Python | Python | JavaScript |
| Pre-commit | Yes | Yes | No |
| GitHub Action | Yes | Community | No |
| AI integration | Pairs with SQL Ops Reviewer | No | No |
sql-sop is not a replacement for sqlfluff. It's a fast first pass that catches 80% of real issues with zero setup. If you need dialect-specific formatting and 800 rules, use sqlfluff. If you want instant feedback on dangerous SQL, use sql-guard.
git clone https://github.com/Pawansingh3889/sql-guard.git
cd sql-guard
pip install -e ".[dev]"
pytest- Create a class in
sql_guard/rules/errors.pyorwarnings.py - Inherit from
Rule, setid,name,severity,description - Override
check_line()for single-line rules orcheck_statement()for multi-line - Add to
ALL_RULESinsql_guard/rules/__init__.py - Add a test in
tests/test_rules.py - Add a trigger case in
tests/fixtures/
class MyNewRule(Rule):
id = "W011"
name = "my-rule"
severity = "warning"
description = "What this rule catches"
multiline = False
_pattern = Rule._compile(r"your regex here")
def check_line(self, line, line_number, file):
if self._pattern.search(line):
return Finding(
rule_id=self.id,
severity=self.severity,
file=file,
line=line_number,
message="What went wrong",
suggestion="How to fix it",
)
return NonePRs welcome. Keep rules simple, keep patterns fast.
MIT