Skip to main content

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sqla_lint-0.1.0.tar.gz (16.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sqla_lint-0.1.0-py3-none-any.whl (17.5 kB view details)

Uploaded Python 3

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

Hashes for sqla_lint-0.1.0.tar.gz
Algorithm Hash digest
SHA256 46d7fb3ffe625bfff839afed4f5b2f49fbb9fa813f2ec2918ac4cb956edb8c3b
MD5 2482226f3999027d90168c35bc39389c
BLAKE2b-256 08c25af236d3459b246087e2d919d7b66143dc47d4eadd238cb8a1843a7261f1

See more details on using hashes here.

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

Hashes for sqla_lint-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cc1c0122a5e1581d74a2b6cdc683e2e07fe9d5b05836871759d5a4f6408991be
MD5 2dd96c8284c826c3cd2d47a1fa41b4cd
BLAKE2b-256 3ba9f9c526e2fa6b18650c6b9b193c4c9e050b9c8fff93040f981f6c1549743b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page