The fastest SQL linter.
50× faster than sqruff. Lint 10,000 SQL files in under a second. Benchmarks
Website ·
Get started ·
Docs ·
Benchmarks
Catch SQL mistakes, apply safe fixes, and format consistently. Add your own rules in Python. SQLClean parses and checks SQL in Rust, and runs your Python Rules on statements it has already parsed.
uv tool install sqlclean # or: pip install sqlclean
sqlclean check reports an unused CTE, a case-sensitive email comparison that a team's custom
Rule forbids, and a = NULL comparison that is never true. sqlclean fix applies the safe fix,
removing the CTE, and formats the file. It leaves = NULL alone because rewriting it to IS NULL
changes which rows match; sqlclean fix --unsafe-fixes applies that fix. The custom Rule only
reports, so its finding stays.
Quick start
sqlclean check # report findings in every .sql file under the directory
sqlclean fix # apply safe fixes, then format
sqlclean format # rewrite files in the canonical layout
Add a sqlclean.toml to choose a dialect, files or Rules. See the
quickstart and
configuration.
Speed
Linting the same 10,282 Snowflake SQL files on a Linux machine with 4 CPUs, each tool with its default rules and no cache:
| Tool | Wall time |
|---|---|
sqlclean check |
0.81 s |
sqruff lint |
41.0 s |
sqlfluff lint --processes 4 |
919 s |
The tools don't check the same things: almost every sqruff and SQLFluff finding comes from a layout rule, which SQLClean leaves to its formatter. See the benchmarks for coverage, the corpus, the method and how to run it yourself.
SQLClean in other configurations, on the same files:
| SQLClean command | Wall time |
|---|---|
sqlclean check --select SQLCLEAN (every built-in Rule) |
1.33 s |
sqlclean check plus three custom Python Rules |
2.27 s |
sqlclean check rerun with nothing changed (result cache) |
0.23 s |
Correct and safe
- Rules for SQL that runs but is wrong. Built-in Rules flag comparisons with
NULL,NOT INlists that containNULL,ROW_NUMBERwith no deterministic order and more. See Rules. - Safe fixes by default.
sqlclean fixapplies safe fixes only. A fix that can change which rows match waits for--unsafe-fixes, and a fix that would break parsing is discarded. Seesqlclean fix. - An idempotent formatter. Every statement gets one canonical layout, and formatting a formatted file changes nothing. See formatting.
Custom Rules in Python
A custom Rule is a Python function over each statement, already parsed and located by the Rust
engine. This one enforces a team convention, not a rule of SQL: the columns the team lists in
EMAIL_COLUMNS are compared case-insensitively. An = or <> that reads one of them, bare or as
LOWER(email), passes only when both sides are LOWER(...) or a lower-case string literal. It
reads comparisons from the statement's syntax tree, so comments and string literals never match,
and it only reports, because changing a comparison is the author's call.
# sql_rules/team.py
import polyglot_sql as ast # pip install "sqlclean[ast]"
from sqlclean import Finding, RuleContext, Statement, rule
EMAIL_COLUMNS = {"email", "billing_email"} # the columns this team treats as email addresses
def email_column(operand: ast.Expression) -> ast.Column | None:
"""The designated email column an operand reads, written bare or as LOWER(column)."""
column = operand.this if isinstance(operand, ast.Lower) else operand
is_email = isinstance(column, ast.Column) and column.name.lower() in EMAIL_COLUMNS
return column if is_email else None
def lowered(operand: ast.Expression) -> bool:
"""Whether an operand is LOWER(...) or a string literal with no upper-case letters."""
literal = isinstance(operand, ast.Literal) and operand.is_string
return isinstance(operand, ast.Lower) or (literal and operand.name == operand.name.lower())
@rule(
code="XSQLCLEANTEAM001",
message="Email comparisons must be case-insensitive",
remediation="Compare LOWER(email) with LOWER(...) instead.",
enabled_by_default=True,
)
def case_insensitive_email(*, statement: Statement, ctx: RuleContext) -> list[Finding]:
starts = {
column.to_dict()["column"]["span"]["start"]
for comparison in statement.polyglot_ast().find_all(ast.Eq, ast.Neq)
for column in map(email_column, (comparison.this, comparison.expression))
if column is not None and not all(map(lowered, (comparison.this, comparison.expression)))
}
return [
ctx.finding(subject=statement, node=column)
for column in statement.columns
if column.start - statement.start in starts
]
Point [rules] custom = ["sql_rules"] in sqlclean.toml at it. The syntax tree needs the ast
extra: pip install "sqlclean[ast]". See
custom Rules,
statement facts and
testing custom Rules.
Integrations
# .pre-commit-config.yaml
repos:
- repo: https://github.com/sqlstack-io/sqlclean
rev: v0.1.0
hooks:
- id: sqlclean-fix
- id: sqlclean-format
Use sqlclean-check where a hook must not change files. In CI, sqlclean check exits non-zero
on findings and writes SARIF with --output-format sarif for code-scanning annotations. See
integrations. sqlclean skills installs a skill for AI
coding agents; see sqlclean skills.
Docs
Development
uv sync --all-groups # builds the native executable into .venv
make check # format, lint, type-check, fensu and clippy
make test # Rust and Python tests
License
Metadata
Release files for sqlclean 0.4.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sqlclean-0.4.2.tar.gz | 2.3 MB | With provenance | Details for sqlclean-0.4.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sqlclean-0.4.2-py3-none-any.whl | Python 3 | none | any | Details for sqlclean-0.4.2-py3-none-any.whl |
Total release size: 2.3 MB
Release files / sqlclean-0.4.2.tar.gz
| Download URL | sqlclean-0.4.2.tar.gz |
|---|---|
| Size | 2.3 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
444f25a3f0c79c03ca7109247c25129df1a6aa6ed9e3e567c008cb9d9c1b2c54
|
|
BLAKE2b-256 checksum How to use checksums |
77b53fee7c358a6020448f0f49696e83765508a35785a6ee5c1249f45a7fb459
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logRelease files / sqlclean-0.4.2-py3-none-any.whl
| Download URL | sqlclean-0.4.2-py3-none-any.whl |
|---|---|
| Size | 30.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
83da03c47cbaac7957ae3d51d3694d5498876dd05dea5dc7be2a4562c482497e
|
|
BLAKE2b-256 checksum How to use checksums |
cc533d83d07efdbd0a02ff185240621915250c9bf3554f4395533be8bc861ba2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logSecurity for sqlclean 0.4.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sqlclean-0.4.2.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 1, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.