Add your description here
Project description
sqla-lint
A linter for SQLAlchemy queries that catches common anti-patterns — both at runtime (against live query objects) and statically (by analysing .py source files without executing them).
Installation
pip install sqla-lint
SQLAlchemy is an optional dependency. Install it alongside the linter to use the runtime API:
pip install "sqla-lint[sqlalchemy]"
CLI
The sqla-lint command performs static analysis on Python source files using AST parsing — no code is executed.
Basic usage
# Lint a single file
sqla-lint path/to/queries.py
# Recurse into a directory
sqla-lint src/
# Use a glob pattern (quote it to prevent shell expansion)
sqla-lint "src/**/*.py"
Options
| Flag | Description |
|---|---|
--skip RULE[,RULE…] |
Skip one or more rule IDs. Repeatable. |
--select RULE[,RULE…] |
Only report these rule IDs. Takes precedence over --skip. |
--min-severity LEVEL |
Minimum severity to report: error, warning, or info (default: info). |
--format text|json |
Output format (default: text). |
-q / --quiet |
Suppress all output; rely on exit code only. |
Examples
# Only report errors and warnings
sqla-lint src/ --min-severity warning
# Skip the "no LIMIT" and "count(*)" rules
sqla-lint src/ --skip SQLA004,SQLA006
# Only check for unbounded DELETEs and UPDATEs
sqla-lint src/ --select SQLA002,SQLA003
# Machine-readable JSON output (e.g. for CI pipelines)
sqla-lint src/ --format json
Exit codes
| Code | Meaning |
|---|---|
0 |
No violations found at or above --min-severity. |
1 |
One or more violations found. |
2 |
Usage / I/O error (no files found, unreadable path, etc.). |
Text output format
myapp/queries.py:14:4: [WARNING] SQLA001: Query uses SELECT * — explicitly list the columns you need
myapp/queries.py:27:8: [ERROR] SQLA002: DELETE statement has no WHERE clause — this will delete all rows in the table
sqla-lint: 2 violations: 1 error, 1 warning
Python API
Runtime linting
Pass a SQLAlchemy query object directly to sqla_lint.lint():
from sqlalchemy import select, text
import sqla_lint
stmt = select(text("*")).select_from(users)
result = sqla_lint.lint(stmt)
for v in result.violations:
print(f"[{v.severity.value.upper()}] {v.rule_id}: {v.message}")
LintResult provides filtered views:
result.has_violations # bool
result.errors # list[LintViolation] — Severity.ERROR only
result.warnings # list[LintViolation] — Severity.WARNING only
result.infos # list[LintViolation] — Severity.INFO only
Static file analysis
Analyse source files without importing or executing them:
from sqla_lint import parse_file, parse_source
# From a file path
for violation in parse_file("myapp/queries.py"):
print(violation) # myapp/queries.py:14:4: [WARNING] SQLA001: ...
# From a source string
violations = parse_source(source_code, filename="<inline>")
FileViolation has rule_id, message, severity, filename, line, and col attributes.
Configuration
Skipping rules
import sqla_lint
config = sqla_lint.LintConfig(skip_rules={"SQLA004", "SQLA006"})
result = sqla_lint.lint(stmt, config=config)
Overriding severity
from sqla_lint import LintConfig, RuleConfig, Severity
config = LintConfig(
rule_configs={
"SQLA004": RuleConfig(severity_override=Severity.ERROR),
}
)
result = sqla_lint.lint(stmt, config=config)
Disabling a rule via RuleConfig
config = LintConfig(
rule_configs={
"SQLA006": RuleConfig(enabled=False),
}
)
Using the Linter class directly
For repeated use (e.g. in tests or middleware), create a Linter instance once:
from sqla_lint import Linter, LintConfig
linter = Linter(config=LintConfig(skip_rules={"SQLA004"}))
result1 = linter.lint(stmt1)
result2 = linter.lint(stmt2)
Rules
| ID | Severity | Description |
|---|---|---|
| SQLA001 | warning | SELECT * — use explicit column names |
| SQLA002 | error | DELETE without WHERE clause — deletes every row |
| SQLA003 | error | UPDATE without WHERE clause — updates every row |
| SQLA004 | warning | SELECT without LIMIT — may return an unbounded result set |
| SQLA005 | warning | LIMIT without ORDER BY — row order is non-deterministic |
| SQLA006 | info | count(*) — prefer count(1) or count(primary_key) |
| SQLA007 | error | Multiple FROM tables without JOIN — Cartesian product (runtime only) |
| SQLA008 | warning | Query inside a loop — potential N+1 (static analysis only) |
SQLA007 requires type information and is only detected by the runtime linter (
sqla_lint.lint()), not by the CLI static analyser.SQLA008 is only detected by the static analyser (CLI /
parse_file) and is not checked at runtime.
Custom rules
Subclass Rule, set the three class attributes, implement check(), and pass an instance to Linter:
from typing import Any
from sqla_lint import Linter, LintConfig, Rule, LintViolation, Severity
class NoOffsetWithoutLimitRule(Rule):
rule_id = "CUSTOM001"
description = "OFFSET without LIMIT is almost always a bug"
default_severity = Severity.ERROR
def check(self, query: Any) -> list[LintViolation]:
offset = getattr(query, "_offset_clause", None)
limit = getattr(query, "_limit_clause", None)
if offset is not None and limit is None:
return [self._violation("OFFSET used without LIMIT")]
return []
linter = Linter(rules=[NoOffsetWithoutLimitRule()])
result = linter.lint(stmt)
To combine custom rules with the built-in ones:
from sqla_lint import ALL_RULES
linter = Linter(rules=list(ALL_RULES) + [NoOffsetWithoutLimitRule()])
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sqla_lint-0.1.0.tar.gz.
File metadata
- Download URL: sqla_lint-0.1.0.tar.gz
- Upload date:
- Size: 16.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
46d7fb3ffe625bfff839afed4f5b2f49fbb9fa813f2ec2918ac4cb956edb8c3b
|
|
| MD5 |
2482226f3999027d90168c35bc39389c
|
|
| BLAKE2b-256 |
08c25af236d3459b246087e2d919d7b66143dc47d4eadd238cb8a1843a7261f1
|
File details
Details for the file sqla_lint-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sqla_lint-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.5.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc1c0122a5e1581d74a2b6cdc683e2e07fe9d5b05836871759d5a4f6408991be
|
|
| MD5 |
2dd96c8284c826c3cd2d47a1fa41b4cd
|
|
| BLAKE2b-256 |
3ba9f9c526e2fa6b18650c6b9b193c4c9e050b9c8fff93040f981f6c1549743b
|