sarj-sql-lint 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ node_modules/
2
+ dist/
3
+ build/
4
+ *.tgz
5
+ .npm/
6
+ *.tsbuildinfo
7
+ .turbo/
8
+ .tsup/
9
+ .vitest-cache/
10
+ .eslintcache
11
+ coverage/
12
+
13
+ __pycache__/
14
+ *.py[co]
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+ .basedpyright/
19
+ .coverage
20
+ .coverage.*
21
+ htmlcov/
22
+ .tox/
23
+ .venv/
24
+ venv/
25
+ *.egg-info/
26
+
27
+ .DS_Store
28
+ Thumbs.db
29
+ .idea/
30
+ .vscode/
31
+ .zed/
32
+ *.swp
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.4
2
+ Name: sarj-sql-lint
3
+ Version: 0.1.0
4
+ Summary: Custom SQL lint rules for Postgres migrations — sqlfluff + pygrep, pre-commit-friendly.
5
+ Project-URL: Homepage, https://github.com/sarj-ai/linting/tree/main/packages/sql
6
+ Project-URL: Repository, https://github.com/sarj-ai/linting
7
+ Project-URL: Issues, https://github.com/sarj-ai/linting/issues
8
+ Author: sarj-ai
9
+ License: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Requires-Python: >=3.13
17
+ Requires-Dist: sqlfluff>=3.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # sarj-sql-lint
21
+
22
+ Custom SQL lint rules for Postgres migrations. AST-based via [sqlfluff](https://sqlfluff.com) for the SARJ101 rule + zero-dependency pygrep regex hooks for migration-hygiene checks. Designed for pre-commit.
23
+
24
+ ```bash
25
+ uv tool install sarj-sql-lint
26
+ ```
27
+
28
+ ## Pre-commit
29
+
30
+ ```yaml
31
+ - repo: https://github.com/sarj-ai/linting
32
+ rev: sql-v0.1.0
33
+ hooks:
34
+ - id: sarj-enforce-timestamptz
35
+ files: '\.sql$'
36
+ - id: sarj-ban-postgres-enums
37
+ files: '\.sql$'
38
+ - id: sarj-ban-create-trigger
39
+ files: '\.sql$'
40
+ - id: sarj-prefer-text-over-varchar
41
+ files: '\.sql$'
42
+ ```
43
+
44
+ ## CLI
45
+
46
+ ```bash
47
+ sarj-sql-lint check --rule enforce-timestamptz svcs/db/db/migrations/*.sql
48
+ sarj-sql-lint list-rules
49
+ ```
50
+
51
+ Diagnostic format is `path:line:col: CODE message` — Ruff-compatible.
52
+
53
+ Each rule's source under `src/sarj_sql_lint/rules/` carries its own `description` and diagnostic message.
@@ -0,0 +1,34 @@
1
+ # sarj-sql-lint
2
+
3
+ Custom SQL lint rules for Postgres migrations. AST-based via [sqlfluff](https://sqlfluff.com) for the SARJ101 rule + zero-dependency pygrep regex hooks for migration-hygiene checks. Designed for pre-commit.
4
+
5
+ ```bash
6
+ uv tool install sarj-sql-lint
7
+ ```
8
+
9
+ ## Pre-commit
10
+
11
+ ```yaml
12
+ - repo: https://github.com/sarj-ai/linting
13
+ rev: sql-v0.1.0
14
+ hooks:
15
+ - id: sarj-enforce-timestamptz
16
+ files: '\.sql$'
17
+ - id: sarj-ban-postgres-enums
18
+ files: '\.sql$'
19
+ - id: sarj-ban-create-trigger
20
+ files: '\.sql$'
21
+ - id: sarj-prefer-text-over-varchar
22
+ files: '\.sql$'
23
+ ```
24
+
25
+ ## CLI
26
+
27
+ ```bash
28
+ sarj-sql-lint check --rule enforce-timestamptz svcs/db/db/migrations/*.sql
29
+ sarj-sql-lint list-rules
30
+ ```
31
+
32
+ Diagnostic format is `path:line:col: CODE message` — Ruff-compatible.
33
+
34
+ Each rule's source under `src/sarj_sql_lint/rules/` carries its own `description` and diagnostic message.
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "sarj-sql-lint"
3
+ version = "0.1.0"
4
+ description = "Custom SQL lint rules for Postgres migrations — sqlfluff + pygrep, pre-commit-friendly."
5
+ readme = "README.md"
6
+ authors = [{ name = "sarj-ai" }]
7
+ license = { text = "MIT" }
8
+ requires-python = ">=3.13"
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Topic :: Software Development :: Quality Assurance",
16
+ ]
17
+ dependencies = [
18
+ "sqlfluff>=3.0",
19
+ ]
20
+
21
+ [project.scripts]
22
+ sarj-sql-lint = "sarj_sql_lint.__main__:main"
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/sarj-ai/linting/tree/main/packages/sql"
26
+ Repository = "https://github.com/sarj-ai/linting"
27
+ Issues = "https://github.com/sarj-ai/linting/issues"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "pytest>=8.0",
32
+ "pytest-benchmark>=4.0",
33
+ "ruff>=0.6",
34
+ "basedpyright>=1.20",
35
+ ]
36
+
37
+ [build-system]
38
+ requires = ["hatchling"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/sarj_sql_lint"]
43
+
44
+ [tool.hatch.build.targets.sdist]
45
+ include = [
46
+ "src",
47
+ "README.md",
48
+ "pyproject.toml",
49
+ ]
50
+ exclude = [
51
+ "tests/",
52
+ "**/__pycache__/",
53
+ "**/*.pyc",
54
+ ]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,87 @@
1
+ """CLI: sarj-sql-lint check --rule <id> [--rule <id2>] <files>"""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from sarj_sql_lint import __version__
9
+ from sarj_sql_lint.rule_base import Diagnostic
10
+ from sarj_sql_lint.rules import REGISTRY
11
+
12
+
13
+ SKIP_DIR_NAMES = {
14
+ "node_modules", ".venv", "venv", ".git", "dist", "build", ".next",
15
+ "coverage", "__pycache__", ".pytest_cache", ".ruff_cache", ".mypy_cache",
16
+ ".turbo", ".yarn", ".pnpm-store",
17
+ }
18
+
19
+
20
+ def _expand_paths(paths: list[Path]) -> list[Path]:
21
+ out: list[Path] = []
22
+ for p in paths:
23
+ if not p.exists():
24
+ continue
25
+ if p.is_file():
26
+ out.append(p)
27
+ continue
28
+ for child in p.rglob("*.sql"):
29
+ if not child.is_file():
30
+ continue
31
+ if any(part in SKIP_DIR_NAMES for part in child.parts):
32
+ continue
33
+ try:
34
+ if child.stat().st_size > 500_000:
35
+ continue
36
+ except OSError:
37
+ continue
38
+ out.append(child)
39
+ return out
40
+
41
+
42
+ def _check(rule_ids: list[str], paths: list[Path]) -> list[Diagnostic]:
43
+ unknown = [rid for rid in rule_ids if rid not in REGISTRY]
44
+ if unknown:
45
+ sys.stderr.write(f"unknown rule(s): {', '.join(unknown)}\n")
46
+ sys.stderr.write(f"available: {', '.join(sorted(REGISTRY))}\n")
47
+ raise SystemExit(2)
48
+ rules = [REGISTRY[rid]() for rid in rule_ids]
49
+ expanded = _expand_paths(paths)
50
+ diags: list[Diagnostic] = []
51
+ for p in expanded:
52
+ try:
53
+ source = p.read_text(encoding="utf-8", errors="replace")
54
+ except OSError:
55
+ continue
56
+ for rule in rules:
57
+ diags.extend(rule.check(p, source))
58
+ return diags
59
+
60
+
61
+ def main(argv: list[str] | None = None) -> int:
62
+ parser = argparse.ArgumentParser(prog="sarj-sql-lint", description="Custom SQL lint rules.")
63
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
64
+ sub = parser.add_subparsers(dest="cmd", required=True)
65
+
66
+ check_p = sub.add_parser("check", help="Run rules over .sql files.")
67
+ check_p.add_argument("--rule", action="append", required=True, help="Rule ID (repeat for multiple).")
68
+ check_p.add_argument("files", nargs="+", type=Path)
69
+
70
+ sub.add_parser("list-rules", help="List available rule IDs.")
71
+
72
+ args = parser.parse_args(argv)
73
+
74
+ if args.cmd == "list-rules":
75
+ for rid, cls in sorted(REGISTRY.items()):
76
+ inst = cls()
77
+ sys.stdout.write(f"{inst.code:8} {rid:40} {inst.description}\n")
78
+ return 0
79
+
80
+ diags = _check(args.rule, args.files)
81
+ for d in diags:
82
+ sys.stdout.write(d.format() + "\n")
83
+ return 1 if diags else 0
84
+
85
+
86
+ if __name__ == "__main__":
87
+ sys.exit(main())
File without changes
@@ -0,0 +1,27 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+
8
+ @dataclass(frozen=True, slots=True)
9
+ class Diagnostic:
10
+ path: Path
11
+ line: int
12
+ col: int
13
+ code: str
14
+ message: str
15
+
16
+ def format(self) -> str:
17
+ return f"{self.path}:{self.line}:{self.col}: {self.code} {self.message}"
18
+
19
+
20
+ class Rule(ABC):
21
+ id: str
22
+ code: str
23
+ description: str
24
+
25
+ @abstractmethod
26
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
27
+ raise NotImplementedError
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from sarj_sql_lint.rule_base import Rule
4
+ from sarj_sql_lint.rules.enforce_timestamptz import EnforceTimestamptz
5
+
6
+
7
+ REGISTRY: dict[str, type[Rule]] = {
8
+ EnforceTimestamptz.id: EnforceTimestamptz,
9
+ }
10
+
11
+ __all__ = ["REGISTRY"]
@@ -0,0 +1,46 @@
1
+ """SARJ101: detect TIMESTAMP columns missing `WITH TIME ZONE`.
2
+
3
+ Postgres `TIMESTAMP` without `WITH TIME ZONE` discards offset on INSERT,
4
+ silently producing wrong timestamps for non-UTC clients. Use TIMESTAMPTZ.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from pathlib import Path
10
+
11
+ from sarj_sql_lint.rule_base import Diagnostic, Rule
12
+
13
+
14
+ PATTERN = re.compile(
15
+ r"\bTIMESTAMP\b(?!\s*TZ\b)(?!\s+WITH\s+TIME\s+ZONE\b)",
16
+ re.IGNORECASE,
17
+ )
18
+
19
+
20
+ class EnforceTimestamptz(Rule):
21
+ """Postgres TIMESTAMP without WITH TIME ZONE — use TIMESTAMPTZ."""
22
+
23
+ id = "enforce-timestamptz"
24
+ code = "SARJ101"
25
+ description = "TIMESTAMP without TIME ZONE — use TIMESTAMPTZ."
26
+
27
+ def check(self, path: Path, source: str) -> list[Diagnostic]:
28
+ diags: list[Diagnostic] = []
29
+ for lineno, line in enumerate(source.splitlines(), start=1):
30
+ stripped = line.lstrip()
31
+ if stripped.startswith("--") or stripped.startswith("/*"):
32
+ continue
33
+ for match in PATTERN.finditer(line):
34
+ diags.append(
35
+ Diagnostic(
36
+ path=path,
37
+ line=lineno,
38
+ col=match.start() + 1,
39
+ code=self.code,
40
+ message=(
41
+ "Use `TIMESTAMPTZ` (or `TIMESTAMP WITH TIME ZONE`) — "
42
+ "naive TIMESTAMP discards offset and is rarely correct."
43
+ ),
44
+ )
45
+ )
46
+ return diags