sqlanvil-sqlx-lint 0.2.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.
- sqlanvil_sqlx_lint-0.2.0/LICENSE +22 -0
- sqlanvil_sqlx_lint-0.2.0/PKG-INFO +187 -0
- sqlanvil_sqlx_lint-0.2.0/README.md +168 -0
- sqlanvil_sqlx_lint-0.2.0/pyproject.toml +34 -0
- sqlanvil_sqlx_lint-0.2.0/setup.cfg +4 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint/__init__.py +16 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint/cli.py +93 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint/config.py +217 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint/linter.py +623 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint.egg-info/PKG-INFO +187 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint.egg-info/SOURCES.txt +15 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint.egg-info/dependency_links.txt +1 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint.egg-info/entry_points.txt +2 -0
- sqlanvil_sqlx_lint-0.2.0/src/sqlanvil_sqlx_lint.egg-info/top_level.txt +1 -0
- sqlanvil_sqlx_lint-0.2.0/tests/test_config.py +107 -0
- sqlanvil_sqlx_lint-0.2.0/tests/test_lint.py +371 -0
- sqlanvil_sqlx_lint-0.2.0/tests/test_sqlanvil_rules.py +327 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Acuantia (original dataform-sqlx-lint)
|
|
4
|
+
Copyright (c) 2026 Ivan Histand / SQLAnvil (sqlanvil-sqlx-lint fork)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sqlanvil-sqlx-lint
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Convention linter for SQLAnvil .sqlx files — config-block, project-convention, and sqlanvil-delta checks that SQL linters and compile cannot see
|
|
5
|
+
Author-email: Ivan Histand <ivan@histand.net>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SQLAnvil/sqlanvil-sqlx-lint
|
|
8
|
+
Project-URL: Documentation, https://sqlanvil.com/docs
|
|
9
|
+
Project-URL: Upstream, https://github.com/acuantia/dataform-sqlx-lint
|
|
10
|
+
Keywords: sqlanvil,sqlx,lint,postgres,supabase,mysql,bigquery,dataform,pre-commit
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# sqlanvil-sqlx-lint
|
|
21
|
+
|
|
22
|
+
A convention linter for [SQLAnvil](https://sqlanvil.com) `.sqlx` files. SQL
|
|
23
|
+
linters (sqlfluff) check the SQL body; `sqlanvil compile` checks syntax and
|
|
24
|
+
`sqlanvil validate` checks against the warehouse. None of them see the
|
|
25
|
+
**config-block and project conventions** that keep a sqlanvil repo healthy, and
|
|
26
|
+
several Dataform habits that sqlanvil silently ignores or fails on at run time.
|
|
27
|
+
This tool does, in milliseconds, with no warehouse connection.
|
|
28
|
+
|
|
29
|
+
Zero dependencies (Python ≥ 3.11 standard library only). Designed for
|
|
30
|
+
[pre-commit](https://pre-commit.com). Warehouse-aware: PostgreSQL, Supabase,
|
|
31
|
+
MySQL/MariaDB, and BigQuery-target projects.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install sqlanvil-sqlx-lint
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Forked from [acuantia/dataform-sqlx-lint](https://github.com/acuantia/dataform-sqlx-lint)
|
|
38
|
+
(MIT), which was extracted from a production Dataform repository.
|
|
39
|
+
|
|
40
|
+
## Rules
|
|
41
|
+
|
|
42
|
+
### Conventions
|
|
43
|
+
|
|
44
|
+
| Code | Default | Checks |
|
|
45
|
+
|------|---------|--------|
|
|
46
|
+
| E001 | on | `config {}` block present and balanced |
|
|
47
|
+
| E002 | on | non-empty `columns: {}` on tables/views/incrementals/declarations (declarations may carry `columnTypes: {}` from `sqlanvil introspect` instead) |
|
|
48
|
+
| E003 | on | `schema:` must not hardcode an environment suffix (`_prod`/`_dev`/`_test` by default) — `--schema-suffix` and `environments.<name>.schemaSuffix` append it, so a literal doubles up (`analytics_test_test`) |
|
|
49
|
+
| E004 | on | `name:` matching the filename is redundant (declarations exempt) |
|
|
50
|
+
| E005 | opt-in | operations/assertions must not set `schema:` (`hasOutput: true` operations exempt — schema+name define `${self()}`) |
|
|
51
|
+
| E006 | on | hardcoded table paths instead of `${ref()}` — these silently break the dependency graph. Warehouse-aware: `public.orders`, `"schema"."table"`, `` `db`.`table` ``, `` `project.dataset.table` `` |
|
|
52
|
+
| E007 | on* | configurable per-directory naming/type policies (*no-op until policies are configured) |
|
|
53
|
+
| W008 | opt-in | `post_operations {}` placed before the main SELECT (style preference) |
|
|
54
|
+
| E010 | on | every determinable output column appears in `columns: {}` — parses the main SELECT conservatively (unparseable expressions are skipped, never false-flagged) and follows `select *` through a single plain `${ref()}` into the upstream file |
|
|
55
|
+
|
|
56
|
+
Why E002/E010 matter: sqlanvil writes `description:` and `columns: {}` into the
|
|
57
|
+
warehouse catalog (`COMMENT ON TABLE` / `COMMENT ON COLUMN` on Postgres, table
|
|
58
|
+
and column descriptions on BigQuery). That is the metadata data catalogs, BI
|
|
59
|
+
tools, and AI analytics agents read. Partial blocks leave silent gaps.
|
|
60
|
+
|
|
61
|
+
### sqlanvil deltas
|
|
62
|
+
|
|
63
|
+
The places where a Dataform/BigQuery habit produces a sqlanvil project that
|
|
64
|
+
compiles but does the wrong thing. The first three are failures no compiler
|
|
65
|
+
catches.
|
|
66
|
+
|
|
67
|
+
| Code | Default | Checks | `sqlanvil compile` catches it? |
|
|
68
|
+
|------|---------|--------|-------------------------------|
|
|
69
|
+
| S101 | on | `bigquery: {}`, `partitionBy`, `clusterBy`, `bigqueryPolicyTags` on a non-BigQuery warehouse — ignored silently, never applied | no |
|
|
70
|
+
| S102 | on | `;` separating statements in `operations` / `pre_operations` / `post_operations` — sqlanvil splits on a `---` line, so the block runs as one statement and fails at run time | no |
|
|
71
|
+
| S105 | on | `ADD PRIMARY KEY` / `ADD CONSTRAINT` in an incremental's operations block not wrapped in `${when(!incremental(), …)}` — runs on every append and errors the second time | no |
|
|
72
|
+
| S103 | on | `postgres.indexes[].method` given as a string — it is a numeric enum (`BTREE=0, HASH=1, GIN=2, GIST=3, BRIN=4`) | yes |
|
|
73
|
+
| S104 | on | `incrementalStrategy` on a non-BigQuery warehouse | yes (≥1.29) |
|
|
74
|
+
| S106 | on | `assertions:` sets both `uniqueKey` and `uniqueKeys` | yes |
|
|
75
|
+
| S108 | on | `.jitCode()` / `jitData()` — no runtime in sqlanvil | yes (≥1.30) |
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
sqlanvil-sqlx-lint definitions/outputs/my_table.sqlx [...]
|
|
81
|
+
# exit 0 = clean or warnings only; 1 = errors; 2 = bad config
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Run from the project root. The target warehouse is read from
|
|
85
|
+
`workflow_settings.yaml` there (`warehouse: postgres|supabase|mysql|bigquery`),
|
|
86
|
+
and `--definitions-root` (default `./definitions`) is indexed so `${ref()}`
|
|
87
|
+
targets resolve for E010's star-resolution. Override with `--warehouse` or the
|
|
88
|
+
`warehouse` config key; the default with nothing configured is `postgres`.
|
|
89
|
+
|
|
90
|
+
### pre-commit
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
repos:
|
|
94
|
+
- repo: https://github.com/SQLAnvil/sqlanvil-sqlx-lint
|
|
95
|
+
rev: v0.2.0
|
|
96
|
+
hooks:
|
|
97
|
+
- id: sqlanvil-sqlx-lint
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Configuration
|
|
101
|
+
|
|
102
|
+
`.sqlx-lint.toml` in the project root, or a `[tool.sqlx-lint]` table in
|
|
103
|
+
`pyproject.toml` (the standalone file wins). All keys optional:
|
|
104
|
+
|
|
105
|
+
```toml
|
|
106
|
+
warehouse = "postgres" # else workflow_settings.yaml, else postgres
|
|
107
|
+
schema_suffixes = ["_prod", "_dev", "_test"] # E003 suffix list ([] disables)
|
|
108
|
+
documented_types = ["table", "view", "incremental", "declaration"] # E002
|
|
109
|
+
coverage_paths = ["definitions/outputs/"] # E010 scope; empty = everywhere
|
|
110
|
+
allowed_schemas = ["extensions"] # E006 may reference these directly
|
|
111
|
+
enable = ["E005", "W008"] # switch on opt-in rules
|
|
112
|
+
disable = ["E004"] # switch off default rules
|
|
113
|
+
|
|
114
|
+
[[dir_policies]] # E007 (repeatable)
|
|
115
|
+
path_contains = "definitions/outputs/"
|
|
116
|
+
require_prefix = "rpt_"
|
|
117
|
+
require_types = ["table", "incremental"]
|
|
118
|
+
severity = "error" # or "warning"
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
See [`examples/sqlx-lint.toml`](examples/sqlx-lint.toml) for a complete
|
|
122
|
+
sources → intermediate → outputs layout. System catalogs (`pg_catalog`,
|
|
123
|
+
`information_schema`, `mysql`, `sys`, …) are always allowed.
|
|
124
|
+
|
|
125
|
+
### Adopting on an existing project
|
|
126
|
+
|
|
127
|
+
A migrated repository will be loud on the first run: E002 on every undocumented
|
|
128
|
+
model and E010 wherever documentation is partial. Adopt in layers rather than
|
|
129
|
+
suppressing:
|
|
130
|
+
|
|
131
|
+
1. Start with `disable = ["E002", "E010"]` so the S-series and E006 findings,
|
|
132
|
+
which are actual defects, land first.
|
|
133
|
+
2. Re-enable E002 and scope E010 with `coverage_paths` to the BI-facing layer.
|
|
134
|
+
3. Widen `coverage_paths` as documentation catches up.
|
|
135
|
+
|
|
136
|
+
### Suppressing findings
|
|
137
|
+
|
|
138
|
+
```sql
|
|
139
|
+
from public.legacy_events -- sqlx-lint: disable=E006 (declaration repoints at cutover)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
or file-wide, anywhere in the file:
|
|
143
|
+
|
|
144
|
+
```sql
|
|
145
|
+
-- sqlx-lint: disable-file=E006
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Suppress with a reason, sparingly — the convention is usually the fix.
|
|
149
|
+
|
|
150
|
+
## Design notes
|
|
151
|
+
|
|
152
|
+
- **Conservative by construction**: the SQL projection parser only claims
|
|
153
|
+
column names it can determine (aliases, simple identifiers, resolvable
|
|
154
|
+
`select *`); anything ambiguous is skipped, so E010 never false-flags.
|
|
155
|
+
- **E006 looks only at table positions** (`FROM`, `JOIN`, `INTO`, `UPDATE`),
|
|
156
|
+
ignores `FROM` used as function syntax (`extract(dow from d.date)`), function
|
|
157
|
+
calls (`public.my_func(1)`), and anything inside `${…}`.
|
|
158
|
+
- **S102 understands sqlanvil's text**: `;` inside string literals, `$$`
|
|
159
|
+
dollar-quoted PL/pgSQL bodies, and `${…}` JavaScript regions never count; a
|
|
160
|
+
single trailing `;` is fine.
|
|
161
|
+
- **Declarations are exempt** from E003/E004 deliberately: raw source schemas
|
|
162
|
+
legitimately carry environment-suffixed names, and `name:` is required.
|
|
163
|
+
- Rule codes are stable; gaps in the numbering are historical.
|
|
164
|
+
|
|
165
|
+
## Agent Skill
|
|
166
|
+
|
|
167
|
+
The `sqlanvil-sqlx-lint` Agent Skill teaches AI coding agents (Claude Code,
|
|
168
|
+
Codex CLI, Cursor, or any tool supporting the open
|
|
169
|
+
[Agent Skills](https://agentskills.io/) format) to run this linter on every
|
|
170
|
+
`.sqlx` file they create or modify. It lives with the other SQLAnvil skills:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npx skills add SQLAnvil/agent-skills
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Source: https://github.com/SQLAnvil/agent-skills
|
|
177
|
+
|
|
178
|
+
## Development
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
python3 -m venv .venv && .venv/bin/pip install -e . pytest
|
|
182
|
+
.venv/bin/pytest # 103 tests
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](https://github.com/SQLAnvil/sqlanvil-sqlx-lint/blob/main/LICENSE).
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# sqlanvil-sqlx-lint
|
|
2
|
+
|
|
3
|
+
A convention linter for [SQLAnvil](https://sqlanvil.com) `.sqlx` files. SQL
|
|
4
|
+
linters (sqlfluff) check the SQL body; `sqlanvil compile` checks syntax and
|
|
5
|
+
`sqlanvil validate` checks against the warehouse. None of them see the
|
|
6
|
+
**config-block and project conventions** that keep a sqlanvil repo healthy, and
|
|
7
|
+
several Dataform habits that sqlanvil silently ignores or fails on at run time.
|
|
8
|
+
This tool does, in milliseconds, with no warehouse connection.
|
|
9
|
+
|
|
10
|
+
Zero dependencies (Python ≥ 3.11 standard library only). Designed for
|
|
11
|
+
[pre-commit](https://pre-commit.com). Warehouse-aware: PostgreSQL, Supabase,
|
|
12
|
+
MySQL/MariaDB, and BigQuery-target projects.
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install sqlanvil-sqlx-lint
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Forked from [acuantia/dataform-sqlx-lint](https://github.com/acuantia/dataform-sqlx-lint)
|
|
19
|
+
(MIT), which was extracted from a production Dataform repository.
|
|
20
|
+
|
|
21
|
+
## Rules
|
|
22
|
+
|
|
23
|
+
### Conventions
|
|
24
|
+
|
|
25
|
+
| Code | Default | Checks |
|
|
26
|
+
|------|---------|--------|
|
|
27
|
+
| E001 | on | `config {}` block present and balanced |
|
|
28
|
+
| E002 | on | non-empty `columns: {}` on tables/views/incrementals/declarations (declarations may carry `columnTypes: {}` from `sqlanvil introspect` instead) |
|
|
29
|
+
| E003 | on | `schema:` must not hardcode an environment suffix (`_prod`/`_dev`/`_test` by default) — `--schema-suffix` and `environments.<name>.schemaSuffix` append it, so a literal doubles up (`analytics_test_test`) |
|
|
30
|
+
| E004 | on | `name:` matching the filename is redundant (declarations exempt) |
|
|
31
|
+
| E005 | opt-in | operations/assertions must not set `schema:` (`hasOutput: true` operations exempt — schema+name define `${self()}`) |
|
|
32
|
+
| E006 | on | hardcoded table paths instead of `${ref()}` — these silently break the dependency graph. Warehouse-aware: `public.orders`, `"schema"."table"`, `` `db`.`table` ``, `` `project.dataset.table` `` |
|
|
33
|
+
| E007 | on* | configurable per-directory naming/type policies (*no-op until policies are configured) |
|
|
34
|
+
| W008 | opt-in | `post_operations {}` placed before the main SELECT (style preference) |
|
|
35
|
+
| E010 | on | every determinable output column appears in `columns: {}` — parses the main SELECT conservatively (unparseable expressions are skipped, never false-flagged) and follows `select *` through a single plain `${ref()}` into the upstream file |
|
|
36
|
+
|
|
37
|
+
Why E002/E010 matter: sqlanvil writes `description:` and `columns: {}` into the
|
|
38
|
+
warehouse catalog (`COMMENT ON TABLE` / `COMMENT ON COLUMN` on Postgres, table
|
|
39
|
+
and column descriptions on BigQuery). That is the metadata data catalogs, BI
|
|
40
|
+
tools, and AI analytics agents read. Partial blocks leave silent gaps.
|
|
41
|
+
|
|
42
|
+
### sqlanvil deltas
|
|
43
|
+
|
|
44
|
+
The places where a Dataform/BigQuery habit produces a sqlanvil project that
|
|
45
|
+
compiles but does the wrong thing. The first three are failures no compiler
|
|
46
|
+
catches.
|
|
47
|
+
|
|
48
|
+
| Code | Default | Checks | `sqlanvil compile` catches it? |
|
|
49
|
+
|------|---------|--------|-------------------------------|
|
|
50
|
+
| S101 | on | `bigquery: {}`, `partitionBy`, `clusterBy`, `bigqueryPolicyTags` on a non-BigQuery warehouse — ignored silently, never applied | no |
|
|
51
|
+
| S102 | on | `;` separating statements in `operations` / `pre_operations` / `post_operations` — sqlanvil splits on a `---` line, so the block runs as one statement and fails at run time | no |
|
|
52
|
+
| S105 | on | `ADD PRIMARY KEY` / `ADD CONSTRAINT` in an incremental's operations block not wrapped in `${when(!incremental(), …)}` — runs on every append and errors the second time | no |
|
|
53
|
+
| S103 | on | `postgres.indexes[].method` given as a string — it is a numeric enum (`BTREE=0, HASH=1, GIN=2, GIST=3, BRIN=4`) | yes |
|
|
54
|
+
| S104 | on | `incrementalStrategy` on a non-BigQuery warehouse | yes (≥1.29) |
|
|
55
|
+
| S106 | on | `assertions:` sets both `uniqueKey` and `uniqueKeys` | yes |
|
|
56
|
+
| S108 | on | `.jitCode()` / `jitData()` — no runtime in sqlanvil | yes (≥1.30) |
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
sqlanvil-sqlx-lint definitions/outputs/my_table.sqlx [...]
|
|
62
|
+
# exit 0 = clean or warnings only; 1 = errors; 2 = bad config
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Run from the project root. The target warehouse is read from
|
|
66
|
+
`workflow_settings.yaml` there (`warehouse: postgres|supabase|mysql|bigquery`),
|
|
67
|
+
and `--definitions-root` (default `./definitions`) is indexed so `${ref()}`
|
|
68
|
+
targets resolve for E010's star-resolution. Override with `--warehouse` or the
|
|
69
|
+
`warehouse` config key; the default with nothing configured is `postgres`.
|
|
70
|
+
|
|
71
|
+
### pre-commit
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
repos:
|
|
75
|
+
- repo: https://github.com/SQLAnvil/sqlanvil-sqlx-lint
|
|
76
|
+
rev: v0.2.0
|
|
77
|
+
hooks:
|
|
78
|
+
- id: sqlanvil-sqlx-lint
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Configuration
|
|
82
|
+
|
|
83
|
+
`.sqlx-lint.toml` in the project root, or a `[tool.sqlx-lint]` table in
|
|
84
|
+
`pyproject.toml` (the standalone file wins). All keys optional:
|
|
85
|
+
|
|
86
|
+
```toml
|
|
87
|
+
warehouse = "postgres" # else workflow_settings.yaml, else postgres
|
|
88
|
+
schema_suffixes = ["_prod", "_dev", "_test"] # E003 suffix list ([] disables)
|
|
89
|
+
documented_types = ["table", "view", "incremental", "declaration"] # E002
|
|
90
|
+
coverage_paths = ["definitions/outputs/"] # E010 scope; empty = everywhere
|
|
91
|
+
allowed_schemas = ["extensions"] # E006 may reference these directly
|
|
92
|
+
enable = ["E005", "W008"] # switch on opt-in rules
|
|
93
|
+
disable = ["E004"] # switch off default rules
|
|
94
|
+
|
|
95
|
+
[[dir_policies]] # E007 (repeatable)
|
|
96
|
+
path_contains = "definitions/outputs/"
|
|
97
|
+
require_prefix = "rpt_"
|
|
98
|
+
require_types = ["table", "incremental"]
|
|
99
|
+
severity = "error" # or "warning"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
See [`examples/sqlx-lint.toml`](examples/sqlx-lint.toml) for a complete
|
|
103
|
+
sources → intermediate → outputs layout. System catalogs (`pg_catalog`,
|
|
104
|
+
`information_schema`, `mysql`, `sys`, …) are always allowed.
|
|
105
|
+
|
|
106
|
+
### Adopting on an existing project
|
|
107
|
+
|
|
108
|
+
A migrated repository will be loud on the first run: E002 on every undocumented
|
|
109
|
+
model and E010 wherever documentation is partial. Adopt in layers rather than
|
|
110
|
+
suppressing:
|
|
111
|
+
|
|
112
|
+
1. Start with `disable = ["E002", "E010"]` so the S-series and E006 findings,
|
|
113
|
+
which are actual defects, land first.
|
|
114
|
+
2. Re-enable E002 and scope E010 with `coverage_paths` to the BI-facing layer.
|
|
115
|
+
3. Widen `coverage_paths` as documentation catches up.
|
|
116
|
+
|
|
117
|
+
### Suppressing findings
|
|
118
|
+
|
|
119
|
+
```sql
|
|
120
|
+
from public.legacy_events -- sqlx-lint: disable=E006 (declaration repoints at cutover)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
or file-wide, anywhere in the file:
|
|
124
|
+
|
|
125
|
+
```sql
|
|
126
|
+
-- sqlx-lint: disable-file=E006
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Suppress with a reason, sparingly — the convention is usually the fix.
|
|
130
|
+
|
|
131
|
+
## Design notes
|
|
132
|
+
|
|
133
|
+
- **Conservative by construction**: the SQL projection parser only claims
|
|
134
|
+
column names it can determine (aliases, simple identifiers, resolvable
|
|
135
|
+
`select *`); anything ambiguous is skipped, so E010 never false-flags.
|
|
136
|
+
- **E006 looks only at table positions** (`FROM`, `JOIN`, `INTO`, `UPDATE`),
|
|
137
|
+
ignores `FROM` used as function syntax (`extract(dow from d.date)`), function
|
|
138
|
+
calls (`public.my_func(1)`), and anything inside `${…}`.
|
|
139
|
+
- **S102 understands sqlanvil's text**: `;` inside string literals, `$$`
|
|
140
|
+
dollar-quoted PL/pgSQL bodies, and `${…}` JavaScript regions never count; a
|
|
141
|
+
single trailing `;` is fine.
|
|
142
|
+
- **Declarations are exempt** from E003/E004 deliberately: raw source schemas
|
|
143
|
+
legitimately carry environment-suffixed names, and `name:` is required.
|
|
144
|
+
- Rule codes are stable; gaps in the numbering are historical.
|
|
145
|
+
|
|
146
|
+
## Agent Skill
|
|
147
|
+
|
|
148
|
+
The `sqlanvil-sqlx-lint` Agent Skill teaches AI coding agents (Claude Code,
|
|
149
|
+
Codex CLI, Cursor, or any tool supporting the open
|
|
150
|
+
[Agent Skills](https://agentskills.io/) format) to run this linter on every
|
|
151
|
+
`.sqlx` file they create or modify. It lives with the other SQLAnvil skills:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
npx skills add SQLAnvil/agent-skills
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Source: https://github.com/SQLAnvil/agent-skills
|
|
158
|
+
|
|
159
|
+
## Development
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
python3 -m venv .venv && .venv/bin/pip install -e . pytest
|
|
163
|
+
.venv/bin/pytest # 103 tests
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT — see [LICENSE](https://github.com/SQLAnvil/sqlanvil-sqlx-lint/blob/main/LICENSE).
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sqlanvil-sqlx-lint"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Convention linter for SQLAnvil .sqlx files — config-block, project-convention, and sqlanvil-delta checks that SQL linters and compile cannot see"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Ivan Histand", email = "ivan@histand.net" }]
|
|
14
|
+
keywords = ["sqlanvil", "sqlx", "lint", "postgres", "supabase", "mysql", "bigquery", "dataform", "pre-commit"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.scripts]
|
|
23
|
+
sqlanvil-sqlx-lint = "sqlanvil_sqlx_lint.cli:entrypoint"
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/SQLAnvil/sqlanvil-sqlx-lint"
|
|
27
|
+
Documentation = "https://sqlanvil.com/docs"
|
|
28
|
+
Upstream = "https://github.com/acuantia/dataform-sqlx-lint"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["src"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""sqlanvil-sqlx-lint: convention linter for SQLAnvil .sqlx files."""
|
|
2
|
+
|
|
3
|
+
from .config import Config, DirPolicy, detect_warehouse, load_config, load_config_file
|
|
4
|
+
from .linter import Finding, lint_file, lint_text
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Config",
|
|
8
|
+
"DirPolicy",
|
|
9
|
+
"Finding",
|
|
10
|
+
"detect_warehouse",
|
|
11
|
+
"lint_file",
|
|
12
|
+
"lint_text",
|
|
13
|
+
"load_config",
|
|
14
|
+
"load_config_file",
|
|
15
|
+
]
|
|
16
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Command-line interface.
|
|
2
|
+
|
|
3
|
+
Usage: sqlanvil-sqlx-lint [--config PATH] [--warehouse NAME]
|
|
4
|
+
[--definitions-root DIR] FILE [FILE ...]
|
|
5
|
+
Exit codes: 0 = clean or warnings only, 1 = errors found, 2 = usage error.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import replace
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .config import WAREHOUSES, Config, load_config, load_config_file, with_detected_warehouse
|
|
16
|
+
from .linter import lint_file
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _repo_resolver(root: Path):
|
|
20
|
+
"""Resolve an action name to the text of <root>/**/<name>.sqlx.
|
|
21
|
+
Used to follow `select *` through ${ref()} for E010."""
|
|
22
|
+
if not root.is_dir():
|
|
23
|
+
return lambda name: None
|
|
24
|
+
index = {p.stem: p for p in root.rglob("*.sqlx")}
|
|
25
|
+
|
|
26
|
+
def resolve(name):
|
|
27
|
+
p = index.get(name)
|
|
28
|
+
try:
|
|
29
|
+
return p.read_text(encoding="utf-8") if p else None
|
|
30
|
+
except OSError:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return resolve
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv: list[str] | None = None) -> int:
|
|
37
|
+
parser = argparse.ArgumentParser(
|
|
38
|
+
prog="sqlanvil-sqlx-lint",
|
|
39
|
+
description="Convention linter for SQLAnvil .sqlx files",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("files", nargs="+", help=".sqlx files to lint")
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--config",
|
|
44
|
+
help="path to a TOML config file (default: .sqlx-lint.toml or "
|
|
45
|
+
"[tool.sqlx-lint] in ./pyproject.toml)",
|
|
46
|
+
)
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"--warehouse",
|
|
49
|
+
choices=WAREHOUSES,
|
|
50
|
+
help="target warehouse (default: `warehouse` in the config, else "
|
|
51
|
+
"workflow_settings.yaml in the working directory, else postgres)",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--definitions-root",
|
|
55
|
+
default="definitions",
|
|
56
|
+
help="directory indexed to resolve ${ref()} targets for the E010 "
|
|
57
|
+
"coverage rule (default: ./definitions)",
|
|
58
|
+
)
|
|
59
|
+
args = parser.parse_args(argv)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
if args.config:
|
|
63
|
+
cfg: Config = with_detected_warehouse(load_config_file(args.config), ".")
|
|
64
|
+
else:
|
|
65
|
+
cfg = load_config(".")
|
|
66
|
+
except (OSError, ValueError) as exc:
|
|
67
|
+
print(f"sqlx-lint: bad config: {exc}", file=sys.stderr)
|
|
68
|
+
return 2
|
|
69
|
+
if args.warehouse:
|
|
70
|
+
cfg = replace(cfg, warehouse=args.warehouse)
|
|
71
|
+
resolver = _repo_resolver(Path(args.definitions_root))
|
|
72
|
+
|
|
73
|
+
errors = warnings = 0
|
|
74
|
+
for path in args.files:
|
|
75
|
+
try:
|
|
76
|
+
findings = lint_file(path, config=cfg, resolver=resolver)
|
|
77
|
+
except OSError as exc:
|
|
78
|
+
print(f"{path}: cannot read: {exc}")
|
|
79
|
+
errors += 1
|
|
80
|
+
continue
|
|
81
|
+
for f in sorted(findings, key=lambda f: f.line):
|
|
82
|
+
print(f"{path}:{f.line}: {f.code} [{f.severity}] {f.message}")
|
|
83
|
+
if f.severity == "error":
|
|
84
|
+
errors += 1
|
|
85
|
+
else:
|
|
86
|
+
warnings += 1
|
|
87
|
+
if errors or warnings:
|
|
88
|
+
print(f"sqlx-lint: {errors} error(s), {warnings} warning(s)")
|
|
89
|
+
return 1 if errors else 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def entrypoint() -> None:
|
|
93
|
+
sys.exit(main())
|