SQL Query Auditor & Optimizer — analyze SQL for performance, security, and best practices
Python License Tests Rules Zero Dependencies
Every SQL query you write is a potential performance bottleneck, security vulnerability, or maintenance headache. Code linters catch bugs in Python, Go, and TypeScript — but who catches bugs in your SQL?
sqlaudit is a zero-dependency SQL static analyzer that catches:
- Performance killers:
SELECT *, missingWHEREonDELETE/UPDATE, leading wildcards inLIKE, excessive JOINs - Security risks: SQL injection patterns,
GRANT ALL, always-true conditions (1=1), comment injection - Correctness bugs:
= NULLinstead ofIS NULL,HAVINGwithoutGROUP BY, implicit column lists - Style issues: inconsistent keyword casing, implicit joins, unnecessary parentheses
No database connection required. No configuration needed. Just point it at SQL and get actionable feedback.
pip install sqlaudit
Or install from source:
git clone https://github.com/JSLEEKR/sqlaudit.git cd sqlaudit pip install -e .
# Audit a SQL file sqlaudit audit -f queries.sql # Audit a single query sqlaudit audit -q "SELECT * FROM users WHERE name = NULL" # Audit from stdin cat migration.sql | sqlaudit audit --stdin # JSON output for CI/CD integration sqlaudit audit -f queries.sql --format json # Only check security rules sqlaudit audit -f queries.sql --category security # Fail CI pipeline on any error or warning sqlaudit audit -f queries.sql --fail-on warning # Show fix suggestions sqlaudit audit -q "DELETE FROM users" --verbose # List all available rules sqlaudit rules
from sqlaudit import Analyzer analyzer = Analyzer() report = analyzer.analyze(""" SELECT * FROM users WHERE name = NULL; DELETE FROM audit_logs; INSERT INTO users VALUES (1, 'test'); """) print(f"Score: {report.score}/100") print(f"Errors: {report.total_errors}") print(f"Warnings: {report.total_warnings}") for query in report.queries: for finding in query.findings: print(f" [{finding.severity.value}] {finding.rule_id}: {finding.message}") print(f" Fix: {finding.suggestion}")
from sqlaudit import Analyzer analyzer = Analyzer() findings = analyzer.check_query("DELETE FROM important_table") for f in findings: print(f"{f.rule_id}: {f.message}") # P002: DELETE without WHERE clause will affect all rows
sqlaudit ships with 27 built-in rules across 5 categories.
| Rule | Severity | Description |
|---|---|---|
| P001 | WARNING | SELECT * retrieves all columns |
| P002 | ERROR | UPDATE/DELETE without WHERE clause |
| P003 | WARNING | Leading wildcard in LIKE prevents index usage |
| P004 | WARNING | Too many JOINs (>5 tables) |
| P005 | WARNING | NOT IN with subquery (NULL handling issues) |
| P006 | INFO | Multiple OR on same column (use IN instead) |
| P007 | HINT | SELECT without LIMIT |
| P008 | WARNING | Function on column in WHERE prevents index usage |
| P009 | HINT | SELECT DISTINCT with JOINs may indicate bad join |
| P010 | INFO | ORDER BY without LIMIT sorts entire result set |
| Rule | Severity | Description |
|---|---|---|
| S001 | ERROR | String concatenation (SQL injection risk) |
| S002 | ERROR | GRANT ALL PRIVILEGES (excessive permissions) |
| S003 | WARNING | DROP without IF EXISTS |
| S004 | WARNING | TRUNCATE detected (no row-level logging) |
| S005 | ERROR | Comment patterns inside string literals |
| S006 | WARNING | UNION injection pattern |
| S007 | WARNING | Always-true condition (1=1, 'a'='a') |
| Rule | Severity | Description |
|---|---|---|
| B001 | WARNING | INSERT without column list |
| B002 | INFO | SELECT INTO (non-standard) |
| B003 | HINT | Multi-table query without aliases |
| B004 | WARNING | Deeply nested subqueries (depth >= 3) |
| B005 | INFO | Implicit join (comma-separated FROM) |
| Rule | Severity | Description |
|---|---|---|
| C001 | ERROR | = NULL instead of IS NULL |
| C002 | WARNING | SELECT * with GROUP BY |
| C003 | ERROR | HAVING without GROUP BY |
| Rule | Severity | Description |
|---|---|---|
| T001 | HINT | Inconsistent keyword casing |
| T002 | HINT | Unnecessary parentheses in simple conditions |
from sqlaudit import Analyzer from sqlaudit.rules import Category # Only security checks analyzer = Analyzer(enable_categories=[Category.SECURITY]) report = analyzer.analyze("SELECT * FROM users")
from sqlaudit import Analyzer from sqlaudit.rules import Severity # Only errors and warnings (skip info/hint) analyzer = Analyzer(min_severity=Severity.WARNING)
analyzer = Analyzer(disable_rules=["P001", "P007", "T001"])
from sqlaudit import Analyzer, Rule, Severity, Category def check_table_prefix(info): """All tables should use 'app_' prefix.""" for table in info.tables: if not table.startswith("app_"): return f"Table '{table}' missing 'app_' prefix" return None analyzer = Analyzer() analyzer.register_rule(Rule( id="CUSTOM001", name="table-prefix", description="Tables must use app_ prefix", severity=Severity.WARNING, category=Category.BEST_PRACTICE, check=check_table_prefix, suggestion="Rename table to use app_ prefix", )) report = analyzer.analyze("SELECT * FROM users") # Finds: Table 'users' missing 'app_' prefix
# Human-readable text (default) sqlaudit audit -q "SELECT * FROM t" --format text # JSON for programmatic consumption sqlaudit audit -q "SELECT * FROM t" --format json # CSV for spreadsheet import sqlaudit audit -q "SELECT * FROM t" --format csv # Summary only (score + counts) sqlaudit audit -q "SELECT * FROM t" --format summary
# GitHub Actions example - name: SQL Audit run: | pip install sqlaudit sqlaudit audit -f migrations/*.sql --format json --fail-on warning
# Pre-commit hook #!/bin/bash SQL_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sql$') if [ -n "$SQL_FILES" ]; then sqlaudit audit -f $SQL_FILES --fail-on error --no-color fi
sqlaudit assigns a score from 0-100 based on findings:
| Severity | Point Deduction |
|---|---|
| ERROR | -10 per finding |
| WARNING | -5 per finding |
| INFO | -2 per finding |
| HINT | -1 per finding |
A pass rate is also computed: percentage of queries with zero errors or warnings.
sqlaudit/
parser.py # SQL tokenizer + query parser (zero dependencies)
rules.py # Rule engine with 27 built-in rules
analyzer.py # Main analyzer (parser + rules + report)
report.py # Report data model (Finding, QueryResult, Report)
formatter.py # Output formatters (text, JSON, CSV, summary)
cli.py # Command-line interface
The architecture is designed for extensibility:
- Parser produces a
QueryInfoobject with structured query metadata - Rules are pure functions:
(QueryInfo) -> Optional[str] - Custom rules can be registered at runtime
- Formatters are pluggable for any output format
MIT