Skip to content

Commit ad1518d

Browse files
JSLEEKRclaude
andcommitted
ship: sqlaudit v1.0.0 (Round 66, SQL Analysis, Python) -- PROJECT #73
SQL Query Auditor & Optimizer with 27 built-in rules across 5 categories (performance, security, best-practice, correctness, style). Zero dependencies. 287 tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 parents  commit ad1518d

20 files changed

Lines changed: 4218 additions & 0 deletions

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.eggs/
2+
*.egg-info/
3+
__pycache__/
4+
*.pyc
5+
dist/
6+
build/
7+
.pytest_cache/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 JSLEEKR
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
<p align="center">
2+
<h1 align="center">sqlaudit</h1>
3+
<p align="center">SQL Query Auditor & Optimizer — analyze SQL for performance, security, and best practices</p>
4+
</p>
5+
6+
<p align="center">
7+
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.9+-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python"></a>
8+
<a href="https://github.com/JSLEEKR/sqlaudit/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green?style=for-the-badge" alt="License"></a>
9+
<a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Tests-287-blue?style=for-the-badge" alt="Tests"></a>
10+
<a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Rules-27-orange?style=for-the-badge" alt="Rules"></a>
11+
<a href="https://github.com/JSLEEKR/sqlaudit"><img src="https://img.shields.io/badge/Zero_Dependencies-brightgreen?style=for-the-badge" alt="Zero Dependencies"></a>
12+
</p>
13+
14+
---
15+
16+
## Why This Exists
17+
18+
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?
19+
20+
**sqlaudit** is a zero-dependency SQL static analyzer that catches:
21+
- **Performance killers**: `SELECT *`, missing `WHERE` on `DELETE/UPDATE`, leading wildcards in `LIKE`, excessive JOINs
22+
- **Security risks**: SQL injection patterns, `GRANT ALL`, always-true conditions (`1=1`), comment injection
23+
- **Correctness bugs**: `= NULL` instead of `IS NULL`, `HAVING` without `GROUP BY`, implicit column lists
24+
- **Style issues**: inconsistent keyword casing, implicit joins, unnecessary parentheses
25+
26+
No database connection required. No configuration needed. Just point it at SQL and get actionable feedback.
27+
28+
---
29+
30+
## Installation
31+
32+
```bash
33+
pip install sqlaudit
34+
```
35+
36+
Or install from source:
37+
38+
```bash
39+
git clone https://github.com/JSLEEKR/sqlaudit.git
40+
cd sqlaudit
41+
pip install -e .
42+
```
43+
44+
---
45+
46+
## Quick Start
47+
48+
### CLI Usage
49+
50+
```bash
51+
# Audit a SQL file
52+
sqlaudit audit -f queries.sql
53+
54+
# Audit a single query
55+
sqlaudit audit -q "SELECT * FROM users WHERE name = NULL"
56+
57+
# Audit from stdin
58+
cat migration.sql | sqlaudit audit --stdin
59+
60+
# JSON output for CI/CD integration
61+
sqlaudit audit -f queries.sql --format json
62+
63+
# Only check security rules
64+
sqlaudit audit -f queries.sql --category security
65+
66+
# Fail CI pipeline on any error or warning
67+
sqlaudit audit -f queries.sql --fail-on warning
68+
69+
# Show fix suggestions
70+
sqlaudit audit -q "DELETE FROM users" --verbose
71+
72+
# List all available rules
73+
sqlaudit rules
74+
```
75+
76+
### Python API
77+
78+
```python
79+
from sqlaudit import Analyzer
80+
81+
analyzer = Analyzer()
82+
report = analyzer.analyze("""
83+
SELECT * FROM users WHERE name = NULL;
84+
DELETE FROM audit_logs;
85+
INSERT INTO users VALUES (1, 'test');
86+
""")
87+
88+
print(f"Score: {report.score}/100")
89+
print(f"Errors: {report.total_errors}")
90+
print(f"Warnings: {report.total_warnings}")
91+
92+
for query in report.queries:
93+
for finding in query.findings:
94+
print(f" [{finding.severity.value}] {finding.rule_id}: {finding.message}")
95+
print(f" Fix: {finding.suggestion}")
96+
```
97+
98+
### Quick Check
99+
100+
```python
101+
from sqlaudit import Analyzer
102+
103+
analyzer = Analyzer()
104+
findings = analyzer.check_query("DELETE FROM important_table")
105+
106+
for f in findings:
107+
print(f"{f.rule_id}: {f.message}")
108+
# P002: DELETE without WHERE clause will affect all rows
109+
```
110+
111+
---
112+
113+
## Rules Reference
114+
115+
sqlaudit ships with 27 built-in rules across 5 categories.
116+
117+
### Performance (P001-P010)
118+
119+
| Rule | Severity | Description |
120+
|------|----------|-------------|
121+
| P001 | WARNING | `SELECT *` retrieves all columns |
122+
| P002 | ERROR | `UPDATE/DELETE` without `WHERE` clause |
123+
| P003 | WARNING | Leading wildcard in `LIKE` prevents index usage |
124+
| P004 | WARNING | Too many JOINs (>5 tables) |
125+
| P005 | WARNING | `NOT IN` with subquery (NULL handling issues) |
126+
| P006 | INFO | Multiple `OR` on same column (use `IN` instead) |
127+
| P007 | HINT | `SELECT` without `LIMIT` |
128+
| P008 | WARNING | Function on column in `WHERE` prevents index usage |
129+
| P009 | HINT | `SELECT DISTINCT` with JOINs may indicate bad join |
130+
| P010 | INFO | `ORDER BY` without `LIMIT` sorts entire result set |
131+
132+
### Security (S001-S007)
133+
134+
| Rule | Severity | Description |
135+
|------|----------|-------------|
136+
| S001 | ERROR | String concatenation (SQL injection risk) |
137+
| S002 | ERROR | `GRANT ALL PRIVILEGES` (excessive permissions) |
138+
| S003 | WARNING | `DROP` without `IF EXISTS` |
139+
| S004 | WARNING | `TRUNCATE` detected (no row-level logging) |
140+
| S005 | ERROR | Comment patterns inside string literals |
141+
| S006 | WARNING | `UNION` injection pattern |
142+
| S007 | WARNING | Always-true condition (`1=1`, `'a'='a'`) |
143+
144+
### Best Practice (B001-B005)
145+
146+
| Rule | Severity | Description |
147+
|------|----------|-------------|
148+
| B001 | WARNING | `INSERT` without column list |
149+
| B002 | INFO | `SELECT INTO` (non-standard) |
150+
| B003 | HINT | Multi-table query without aliases |
151+
| B004 | WARNING | Deeply nested subqueries (depth >= 3) |
152+
| B005 | INFO | Implicit join (comma-separated `FROM`) |
153+
154+
### Correctness (C001-C003)
155+
156+
| Rule | Severity | Description |
157+
|------|----------|-------------|
158+
| C001 | ERROR | `= NULL` instead of `IS NULL` |
159+
| C002 | WARNING | `SELECT *` with `GROUP BY` |
160+
| C003 | ERROR | `HAVING` without `GROUP BY` |
161+
162+
### Style (T001-T002)
163+
164+
| Rule | Severity | Description |
165+
|------|----------|-------------|
166+
| T001 | HINT | Inconsistent keyword casing |
167+
| T002 | HINT | Unnecessary parentheses in simple conditions |
168+
169+
---
170+
171+
## Advanced Usage
172+
173+
### Category Filtering
174+
175+
```python
176+
from sqlaudit import Analyzer
177+
from sqlaudit.rules import Category
178+
179+
# Only security checks
180+
analyzer = Analyzer(enable_categories=[Category.SECURITY])
181+
report = analyzer.analyze("SELECT * FROM users")
182+
```
183+
184+
### Severity Filtering
185+
186+
```python
187+
from sqlaudit import Analyzer
188+
from sqlaudit.rules import Severity
189+
190+
# Only errors and warnings (skip info/hint)
191+
analyzer = Analyzer(min_severity=Severity.WARNING)
192+
```
193+
194+
### Disable Specific Rules
195+
196+
```python
197+
analyzer = Analyzer(disable_rules=["P001", "P007", "T001"])
198+
```
199+
200+
### Custom Rules
201+
202+
```python
203+
from sqlaudit import Analyzer, Rule, Severity, Category
204+
205+
def check_table_prefix(info):
206+
"""All tables should use 'app_' prefix."""
207+
for table in info.tables:
208+
if not table.startswith("app_"):
209+
return f"Table '{table}' missing 'app_' prefix"
210+
return None
211+
212+
analyzer = Analyzer()
213+
analyzer.register_rule(Rule(
214+
id="CUSTOM001",
215+
name="table-prefix",
216+
description="Tables must use app_ prefix",
217+
severity=Severity.WARNING,
218+
category=Category.BEST_PRACTICE,
219+
check=check_table_prefix,
220+
suggestion="Rename table to use app_ prefix",
221+
))
222+
223+
report = analyzer.analyze("SELECT * FROM users")
224+
# Finds: Table 'users' missing 'app_' prefix
225+
```
226+
227+
### Output Formats
228+
229+
```bash
230+
# Human-readable text (default)
231+
sqlaudit audit -q "SELECT * FROM t" --format text
232+
233+
# JSON for programmatic consumption
234+
sqlaudit audit -q "SELECT * FROM t" --format json
235+
236+
# CSV for spreadsheet import
237+
sqlaudit audit -q "SELECT * FROM t" --format csv
238+
239+
# Summary only (score + counts)
240+
sqlaudit audit -q "SELECT * FROM t" --format summary
241+
```
242+
243+
### CI/CD Integration
244+
245+
```yaml
246+
# GitHub Actions example
247+
- name: SQL Audit
248+
run: |
249+
pip install sqlaudit
250+
sqlaudit audit -f migrations/*.sql --format json --fail-on warning
251+
```
252+
253+
```bash
254+
# Pre-commit hook
255+
#!/bin/bash
256+
SQL_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.sql$')
257+
if [ -n "$SQL_FILES" ]; then
258+
sqlaudit audit -f $SQL_FILES --fail-on error --no-color
259+
fi
260+
```
261+
262+
---
263+
264+
## Scoring
265+
266+
sqlaudit assigns a score from 0-100 based on findings:
267+
268+
| Severity | Point Deduction |
269+
|----------|----------------|
270+
| ERROR | -10 per finding |
271+
| WARNING | -5 per finding |
272+
| INFO | -2 per finding |
273+
| HINT | -1 per finding |
274+
275+
A **pass rate** is also computed: percentage of queries with zero errors or warnings.
276+
277+
---
278+
279+
## Architecture
280+
281+
```
282+
sqlaudit/
283+
parser.py # SQL tokenizer + query parser (zero dependencies)
284+
rules.py # Rule engine with 27 built-in rules
285+
analyzer.py # Main analyzer (parser + rules + report)
286+
report.py # Report data model (Finding, QueryResult, Report)
287+
formatter.py # Output formatters (text, JSON, CSV, summary)
288+
cli.py # Command-line interface
289+
```
290+
291+
The architecture is designed for extensibility:
292+
- **Parser** produces a `QueryInfo` object with structured query metadata
293+
- **Rules** are pure functions: `(QueryInfo) -> Optional[str]`
294+
- **Custom rules** can be registered at runtime
295+
- **Formatters** are pluggable for any output format
296+
297+
---
298+
299+
## License
300+
301+
MIT

pyproject.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
[build-system]
2+
requires = ["setuptools>=68.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "sqlaudit"
7+
version = "1.0.0"
8+
description = "SQL Query Auditor & Optimizer — analyze SQL for performance, security, and best practices"
9+
readme = "README.md"
10+
license = {text = "MIT"}
11+
requires-python = ">=3.9"
12+
authors = [{name = "JSLEEKR", email = "93jslee@gmail.com"}]
13+
keywords = ["sql", "audit", "security", "performance", "optimizer", "linter"]
14+
classifiers = [
15+
"Development Status :: 5 - Production/Stable",
16+
"Intended Audience :: Developers",
17+
"License :: OSI Approved :: MIT License",
18+
"Programming Language :: Python :: 3",
19+
"Programming Language :: Python :: 3.9",
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Topic :: Database",
24+
"Topic :: Software Development :: Quality Assurance",
25+
]
26+
27+
[project.scripts]
28+
sqlaudit = "sqlaudit.cli:main"
29+
30+
[project.optional-dependencies]
31+
dev = ["pytest>=7.0", "pytest-cov"]
32+
33+
[tool.setuptools.packages.find]
34+
where = ["src"]
35+
36+
[tool.pytest.ini_options]
37+
testpaths = ["tests"]

src/sqlaudit/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""sqlaudit - SQL Query Auditor & Optimizer."""
2+
3+
__version__ = "1.0.0"
4+
5+
from sqlaudit.analyzer import Analyzer
6+
from sqlaudit.rules import RuleEngine, Rule, Severity, Category
7+
from sqlaudit.report import Report, Finding
8+
from sqlaudit.formatter import format_report
9+
10+
__all__ = [
11+
"Analyzer",
12+
"RuleEngine",
13+
"Rule",
14+
"Severity",
15+
"Category",
16+
"Report",
17+
"Finding",
18+
"format_report",
19+
]

src/sqlaudit/__main__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Allow running as python -m sqlaudit."""
2+
3+
from sqlaudit.cli import main
4+
5+
if __name__ == "__main__":
6+
main()

0 commit comments

Comments
 (0)