Skip to main content

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()])

Release files for sqla-lint 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sqla-lint 0.1.0
File Size Uploaded
sqla_lint-0.1.0.tar.gz 16.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sqla-lint 0.1.0
File Interpreter ABI Platform
sqla_lint-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size:34.1 kB

Release files / sqla_lint-0.1.0.tar.gz

Download URL sqla_lint-0.1.0.tar.gz
Size 16.6 kB
Tags Source
SHA-256 checksum
How to use checksums
46d7fb3ffe625bfff839afed4f5b2f49fbb9fa813f2ec2918ac4cb956edb8c3b
BLAKE2b-256 checksum
How to use checksums
08c25af236d3459b246087e2d919d7b66143dc47d4eadd238cb8a1843a7261f1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.11

Release files / sqla_lint-0.1.0-py3-none-any.whl

Download URL sqla_lint-0.1.0-py3-none-any.whl
Size 17.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cc1c0122a5e1581d74a2b6cdc683e2e07fe9d5b05836871759d5a4f6408991be
BLAKE2b-256 checksum
How to use checksums
3ba9f9c526e2fa6b18650c6b9b193c4c9e050b9c8fff93040f981f6c1549743b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.5.11

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page