sql-diff-explainer 0.1.2__tar.gz → 0.1.4__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.
- sql_diff_explainer-0.1.4/PKG-INFO +284 -0
- sql_diff_explainer-0.1.4/README.md +270 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/pyproject.toml +1 -1
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/src/sql_diff_explainer/__init__.py +12 -1
- sql_diff_explainer-0.1.4/src/sql_diff_explainer/cli.py +146 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/src/sql_diff_explainer/core.py +256 -0
- sql_diff_explainer-0.1.4/src/sql_diff_explainer/reports.py +219 -0
- sql_diff_explainer-0.1.4/tests/test_core.py +534 -0
- sql_diff_explainer-0.1.2/PKG-INFO +0 -147
- sql_diff_explainer-0.1.2/README.md +0 -133
- sql_diff_explainer-0.1.2/src/sql_diff_explainer/cli.py +0 -60
- sql_diff_explainer-0.1.2/tests/test_core.py +0 -204
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/.github/workflows/ci.yml +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/.gitignore +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/LICENSE +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/after.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/aggregation_after.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/aggregation_before.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/before.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/distinct_after.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/distinct_before.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/filter_after.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/filter_before.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/join_after.sql +0 -0
- {sql_diff_explainer-0.1.2 → sql_diff_explainer-0.1.4}/examples/join_before.sql +0 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sql-diff-explainer
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: Explain SQL schema and migration differences
|
|
5
|
+
Author: zmodz1010
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: sqlglot>=25.0
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# SQL Diff Explainer
|
|
16
|
+
|
|
17
|
+
**See what changed in a SQL query—and why it might matter.**
|
|
18
|
+
|
|
19
|
+
SQL Diff Explainer compares the structure of two `SELECT` queries and returns
|
|
20
|
+
the changes with a static risk signal, so reviews focus on query behavior rather
|
|
21
|
+
than formatting.
|
|
22
|
+
|
|
23
|
+
[](https://pypi.org/project/sql-diff-explainer/)
|
|
24
|
+
[](https://pypi.org/project/sql-diff-explainer/)
|
|
25
|
+
[](LICENSE)
|
|
26
|
+
|
|
27
|
+
## Contents
|
|
28
|
+
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [Command-line usage](#command-line-usage)
|
|
31
|
+
- [Directory comparison](#directory-comparison)
|
|
32
|
+
- [Git-aware pull-request comparison](#git-aware-pull-request-comparison)
|
|
33
|
+
- [SARIF and HTML reports](#sarif-and-html-reports)
|
|
34
|
+
- [What it compares](#what-it-compares)
|
|
35
|
+
- [Use cases](#use-cases)
|
|
36
|
+
- [Scope and limitations](#scope-and-limitations)
|
|
37
|
+
- [Development](#development)
|
|
38
|
+
- [Contributing](#contributing)
|
|
39
|
+
- [License](#license)
|
|
40
|
+
|
|
41
|
+
## Quick start
|
|
42
|
+
|
|
43
|
+
Install the latest release from [PyPI](https://pypi.org/project/sql-diff-explainer/):
|
|
44
|
+
|
|
45
|
+
```console
|
|
46
|
+
python -m pip install sql-diff-explainer
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Compare two queries from Python:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from sql_diff_explainer import explain_sql_change
|
|
53
|
+
|
|
54
|
+
before = """
|
|
55
|
+
SELECT c.customer_id, c.segment, o.order_id
|
|
56
|
+
FROM customers AS c
|
|
57
|
+
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
|
|
58
|
+
"""
|
|
59
|
+
after = """
|
|
60
|
+
SELECT c.customer_id, c.segment, o.order_id
|
|
61
|
+
FROM customers AS c
|
|
62
|
+
INNER JOIN orders AS o ON c.customer_id = o.customer_id
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
result = explain_sql_change(before, after)
|
|
66
|
+
print(result.summary)
|
|
67
|
+
for change in result.changes:
|
|
68
|
+
print(
|
|
69
|
+
f"{change.category}: {change.before} -> {change.after} "
|
|
70
|
+
f"({change.risk} risk)"
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Output:
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
1 SQL change(s) detected; overall risk is high.
|
|
78
|
+
join: LEFT JOIN orders AS o ON c.customer_id = o.customer_id -> INNER JOIN orders AS o ON c.customer_id = o.customer_id (high risk)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The change is classified as high risk because an `INNER JOIN` excludes
|
|
82
|
+
customers without a matching order, whereas a `LEFT JOIN` retains them. The
|
|
83
|
+
result is structured: inspect `result.risk_level` and `result.changes`, or call
|
|
84
|
+
`result.to_dict()` for JSON-ready data. Pass `dialect="postgres"` (or another
|
|
85
|
+
[sqlglot dialect](https://sqlglot.com/sqlglot/dialects.html)) for
|
|
86
|
+
dialect-specific SQL. Invalid SQL raises `SQLParseError`; non-`SELECT` input
|
|
87
|
+
raises `UnsupportedSQL`.
|
|
88
|
+
|
|
89
|
+
## Command-line usage
|
|
90
|
+
|
|
91
|
+
The CLI compares two SQL files and prints a human-readable report by default:
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
sql-diff-explainer examples/join_before.sql examples/join_after.sql
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Choose JSON output, set a parsing dialect, or make a risk threshold fail a CI
|
|
98
|
+
step:
|
|
99
|
+
|
|
100
|
+
```console
|
|
101
|
+
sql-diff-explainer before.sql after.sql --format json
|
|
102
|
+
sql-diff-explainer before.sql after.sql --dialect postgres
|
|
103
|
+
sql-diff-explainer before.sql after.sql --fail-on-risk medium
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--fail-on-risk` accepts `low`, `medium`, or `high`. It exits with status `1`
|
|
107
|
+
when detected changes meet or exceed the threshold; the text or JSON report is
|
|
108
|
+
still printed. No detected changes exit successfully. Input-file errors and
|
|
109
|
+
SQL parse/support errors exit with status `2`.
|
|
110
|
+
|
|
111
|
+
Try the runnable before/after pairs in [`examples/`](examples/):
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
sql-diff-explainer examples/filter_before.sql examples/filter_after.sql --format json
|
|
115
|
+
sql-diff-explainer examples/aggregation_before.sql examples/aggregation_after.sql
|
|
116
|
+
sql-diff-explainer examples/distinct_before.sql examples/distinct_after.sql
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Directory comparison
|
|
120
|
+
|
|
121
|
+
Compare two directory trees recursively. SQL files are paired by their exact
|
|
122
|
+
relative path (with `/` separators); the `.sql` extension is matched
|
|
123
|
+
case-insensitively. Files present on only one side are reported as additions
|
|
124
|
+
or removals rather than parsed as empty SQL, and raise the aggregate risk to
|
|
125
|
+
high:
|
|
126
|
+
|
|
127
|
+
```console
|
|
128
|
+
sql-diff-explainer --compare-dirs models/main models/review --format json
|
|
129
|
+
sql-diff-explainer --compare-dirs models/main models/review --fail-on-risk high
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The JSON report includes each matched file's ordinary SQL diff, plus
|
|
133
|
+
`unmatched_before` and `unmatched_after` path lists. A missing or non-directory
|
|
134
|
+
input, unreadable file, or invalid SQL exits with status `2`.
|
|
135
|
+
|
|
136
|
+
The same operation is available in Python:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from sql_diff_explainer import compare_sql_directories
|
|
140
|
+
|
|
141
|
+
result = compare_sql_directories("models/main", "models/review")
|
|
142
|
+
print(result.to_dict())
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Git-aware pull-request comparison
|
|
146
|
+
|
|
147
|
+
Compare SQL paths Git reports as changed between a base ref and the current
|
|
148
|
+
worktree. Modified files and SQL-to-SQL renames are compared semantically;
|
|
149
|
+
added or removed SQL files are reported explicitly as high risk. Untracked
|
|
150
|
+
files are not included. The base can be a branch, tag, or commit:
|
|
151
|
+
|
|
152
|
+
```console
|
|
153
|
+
sql-diff-explainer --git-base origin/main --format json
|
|
154
|
+
sql-diff-explainer --git-base "$GITHUB_BASE_REF" --format sarif --output sql-diff.sarif
|
|
155
|
+
sql-diff-explainer --git-base origin/main --fail-on-risk high
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
For GitHub Actions, fetch the base history and use the pull request's base
|
|
159
|
+
commit. This example checks out the pull-request worktree, installs this
|
|
160
|
+
repository's source (so the CLI includes the feature), emits SARIF, and fails
|
|
161
|
+
the job on high-risk changes:
|
|
162
|
+
|
|
163
|
+
```yaml
|
|
164
|
+
name: SQL review
|
|
165
|
+
on:
|
|
166
|
+
pull_request:
|
|
167
|
+
|
|
168
|
+
jobs:
|
|
169
|
+
sql-diff:
|
|
170
|
+
runs-on: ubuntu-latest
|
|
171
|
+
steps:
|
|
172
|
+
- uses: actions/checkout@v4
|
|
173
|
+
with:
|
|
174
|
+
fetch-depth: 0
|
|
175
|
+
- uses: actions/setup-python@v5
|
|
176
|
+
with:
|
|
177
|
+
python-version: "3.12"
|
|
178
|
+
- run: python -m pip install .
|
|
179
|
+
- name: Analyze changed SQL
|
|
180
|
+
run: >-
|
|
181
|
+
sql-diff-explainer
|
|
182
|
+
--git-base "${{ github.event.pull_request.base.sha }}"
|
|
183
|
+
--format sarif
|
|
184
|
+
--output sql-diff.sarif
|
|
185
|
+
--fail-on-risk high
|
|
186
|
+
- name: Upload SQL findings
|
|
187
|
+
if: always() && hashFiles('sql-diff.sarif') != ''
|
|
188
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
189
|
+
with:
|
|
190
|
+
sarif_file: sql-diff.sarif
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
An unresolved base ref, Git failure, unreadable SQL, or invalid SQL exits with
|
|
194
|
+
status `2` and a diagnostic. Renames appear in every report; a rename does not
|
|
195
|
+
raise risk by itself, while semantic changes in its content retain their
|
|
196
|
+
normal risk level.
|
|
197
|
+
|
|
198
|
+
### SARIF and HTML reports
|
|
199
|
+
|
|
200
|
+
Emit SARIF 2.1.0 results for GitHub code scanning integrations, or generate a
|
|
201
|
+
self-contained HTML report that can be attached to a review:
|
|
202
|
+
|
|
203
|
+
```console
|
|
204
|
+
sql-diff-explainer before.sql after.sql --format sarif --output sql-diff.sarif
|
|
205
|
+
sql-diff-explainer --compare-dirs models/main models/review --format html --output review.html
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Both formats work with the existing `--fail-on-risk` threshold. SARIF maps
|
|
209
|
+
high, medium, and low risk to `error`, `warning`, and `note`; report output is
|
|
210
|
+
still written when the threshold causes exit status `1`. HTML escapes SQL text
|
|
211
|
+
and file paths. Python callers can use `to_sarif(result)` or
|
|
212
|
+
`render_html_report(result)` with either `SQLDiffResult` or
|
|
213
|
+
`SQLDirectoryDiffResult`.
|
|
214
|
+
|
|
215
|
+
## What it compares
|
|
216
|
+
|
|
217
|
+
The analyzer compares supported `SELECT` query features and reports additions,
|
|
218
|
+
removals, or changes with a low, medium, or high risk classification:
|
|
219
|
+
|
|
220
|
+
| Feature | Examples of changes |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| Projections | Selected columns and expressions |
|
|
223
|
+
| Joins | Join target, predicate, or type |
|
|
224
|
+
| Filters | `WHERE` predicates |
|
|
225
|
+
| Aggregation | Aggregate functions and `GROUP BY` expressions |
|
|
226
|
+
| Ordering and pagination | `ORDER BY`, `LIMIT`, and `OFFSET` |
|
|
227
|
+
| Duplicate handling | `DISTINCT` added or removed |
|
|
228
|
+
|
|
229
|
+
Join or filter changes can alter which rows qualify; grouping or aggregation
|
|
230
|
+
changes can change a metric's meaning or result grain; and pagination changes
|
|
231
|
+
can affect which rows downstream consumers receive.
|
|
232
|
+
|
|
233
|
+
## Use cases
|
|
234
|
+
|
|
235
|
+
- Review dbt models or warehouse SQL in a pull request.
|
|
236
|
+
- Spot join and filter changes that could alter row counts.
|
|
237
|
+
- Check grouping and aggregation changes that could shift metric grain.
|
|
238
|
+
- Add a lightweight risk threshold to a CI check.
|
|
239
|
+
- Explain generated SQL changes without treating formatting-only differences
|
|
240
|
+
as query changes.
|
|
241
|
+
|
|
242
|
+
## Scope and limitations
|
|
243
|
+
|
|
244
|
+
This is static analysis, not a SQL execution engine or correctness proof. It
|
|
245
|
+
does not connect to a database, validate schemas or permissions, estimate query
|
|
246
|
+
plans or cost, inspect table statistics or null rates, or decide whether a
|
|
247
|
+
business metric is correct. It compares supported query structure; it does not
|
|
248
|
+
analyze a multi-statement migration file or guarantee complete semantic
|
|
249
|
+
equivalence across SQL dialects. Risk levels are review signals, not
|
|
250
|
+
predictions. Pair the report with warehouse tests, representative row-count
|
|
251
|
+
checks, query-plan review, and domain-owner input.
|
|
252
|
+
|
|
253
|
+
## Development
|
|
254
|
+
|
|
255
|
+
The package requires Python 3.10 or newer and uses a `src/` layout.
|
|
256
|
+
|
|
257
|
+
```console
|
|
258
|
+
python -m venv .venv
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Activate the environment, then install the development dependencies and run
|
|
262
|
+
the checks:
|
|
263
|
+
|
|
264
|
+
```console
|
|
265
|
+
# macOS / Linux
|
|
266
|
+
source .venv/bin/activate
|
|
267
|
+
|
|
268
|
+
# Windows PowerShell
|
|
269
|
+
.venv\Scripts\Activate.ps1
|
|
270
|
+
|
|
271
|
+
python -m pip install -e ".[dev]"
|
|
272
|
+
python -m pytest
|
|
273
|
+
ruff check .
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Contributing
|
|
277
|
+
|
|
278
|
+
Bug reports, focused improvements, and pull requests are welcome. Please
|
|
279
|
+
include or update tests for behavior changes and keep documentation aligned
|
|
280
|
+
with the supported API.
|
|
281
|
+
|
|
282
|
+
## License
|
|
283
|
+
|
|
284
|
+
Distributed under the [MIT License](LICENSE).
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# SQL Diff Explainer
|
|
2
|
+
|
|
3
|
+
**See what changed in a SQL query—and why it might matter.**
|
|
4
|
+
|
|
5
|
+
SQL Diff Explainer compares the structure of two `SELECT` queries and returns
|
|
6
|
+
the changes with a static risk signal, so reviews focus on query behavior rather
|
|
7
|
+
than formatting.
|
|
8
|
+
|
|
9
|
+
[](https://pypi.org/project/sql-diff-explainer/)
|
|
10
|
+
[](https://pypi.org/project/sql-diff-explainer/)
|
|
11
|
+
[](LICENSE)
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Quick start](#quick-start)
|
|
16
|
+
- [Command-line usage](#command-line-usage)
|
|
17
|
+
- [Directory comparison](#directory-comparison)
|
|
18
|
+
- [Git-aware pull-request comparison](#git-aware-pull-request-comparison)
|
|
19
|
+
- [SARIF and HTML reports](#sarif-and-html-reports)
|
|
20
|
+
- [What it compares](#what-it-compares)
|
|
21
|
+
- [Use cases](#use-cases)
|
|
22
|
+
- [Scope and limitations](#scope-and-limitations)
|
|
23
|
+
- [Development](#development)
|
|
24
|
+
- [Contributing](#contributing)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
Install the latest release from [PyPI](https://pypi.org/project/sql-diff-explainer/):
|
|
30
|
+
|
|
31
|
+
```console
|
|
32
|
+
python -m pip install sql-diff-explainer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Compare two queries from Python:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from sql_diff_explainer import explain_sql_change
|
|
39
|
+
|
|
40
|
+
before = """
|
|
41
|
+
SELECT c.customer_id, c.segment, o.order_id
|
|
42
|
+
FROM customers AS c
|
|
43
|
+
LEFT JOIN orders AS o ON c.customer_id = o.customer_id
|
|
44
|
+
"""
|
|
45
|
+
after = """
|
|
46
|
+
SELECT c.customer_id, c.segment, o.order_id
|
|
47
|
+
FROM customers AS c
|
|
48
|
+
INNER JOIN orders AS o ON c.customer_id = o.customer_id
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
result = explain_sql_change(before, after)
|
|
52
|
+
print(result.summary)
|
|
53
|
+
for change in result.changes:
|
|
54
|
+
print(
|
|
55
|
+
f"{change.category}: {change.before} -> {change.after} "
|
|
56
|
+
f"({change.risk} risk)"
|
|
57
|
+
)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Output:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
1 SQL change(s) detected; overall risk is high.
|
|
64
|
+
join: LEFT JOIN orders AS o ON c.customer_id = o.customer_id -> INNER JOIN orders AS o ON c.customer_id = o.customer_id (high risk)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The change is classified as high risk because an `INNER JOIN` excludes
|
|
68
|
+
customers without a matching order, whereas a `LEFT JOIN` retains them. The
|
|
69
|
+
result is structured: inspect `result.risk_level` and `result.changes`, or call
|
|
70
|
+
`result.to_dict()` for JSON-ready data. Pass `dialect="postgres"` (or another
|
|
71
|
+
[sqlglot dialect](https://sqlglot.com/sqlglot/dialects.html)) for
|
|
72
|
+
dialect-specific SQL. Invalid SQL raises `SQLParseError`; non-`SELECT` input
|
|
73
|
+
raises `UnsupportedSQL`.
|
|
74
|
+
|
|
75
|
+
## Command-line usage
|
|
76
|
+
|
|
77
|
+
The CLI compares two SQL files and prints a human-readable report by default:
|
|
78
|
+
|
|
79
|
+
```console
|
|
80
|
+
sql-diff-explainer examples/join_before.sql examples/join_after.sql
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Choose JSON output, set a parsing dialect, or make a risk threshold fail a CI
|
|
84
|
+
step:
|
|
85
|
+
|
|
86
|
+
```console
|
|
87
|
+
sql-diff-explainer before.sql after.sql --format json
|
|
88
|
+
sql-diff-explainer before.sql after.sql --dialect postgres
|
|
89
|
+
sql-diff-explainer before.sql after.sql --fail-on-risk medium
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`--fail-on-risk` accepts `low`, `medium`, or `high`. It exits with status `1`
|
|
93
|
+
when detected changes meet or exceed the threshold; the text or JSON report is
|
|
94
|
+
still printed. No detected changes exit successfully. Input-file errors and
|
|
95
|
+
SQL parse/support errors exit with status `2`.
|
|
96
|
+
|
|
97
|
+
Try the runnable before/after pairs in [`examples/`](examples/):
|
|
98
|
+
|
|
99
|
+
```console
|
|
100
|
+
sql-diff-explainer examples/filter_before.sql examples/filter_after.sql --format json
|
|
101
|
+
sql-diff-explainer examples/aggregation_before.sql examples/aggregation_after.sql
|
|
102
|
+
sql-diff-explainer examples/distinct_before.sql examples/distinct_after.sql
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Directory comparison
|
|
106
|
+
|
|
107
|
+
Compare two directory trees recursively. SQL files are paired by their exact
|
|
108
|
+
relative path (with `/` separators); the `.sql` extension is matched
|
|
109
|
+
case-insensitively. Files present on only one side are reported as additions
|
|
110
|
+
or removals rather than parsed as empty SQL, and raise the aggregate risk to
|
|
111
|
+
high:
|
|
112
|
+
|
|
113
|
+
```console
|
|
114
|
+
sql-diff-explainer --compare-dirs models/main models/review --format json
|
|
115
|
+
sql-diff-explainer --compare-dirs models/main models/review --fail-on-risk high
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The JSON report includes each matched file's ordinary SQL diff, plus
|
|
119
|
+
`unmatched_before` and `unmatched_after` path lists. A missing or non-directory
|
|
120
|
+
input, unreadable file, or invalid SQL exits with status `2`.
|
|
121
|
+
|
|
122
|
+
The same operation is available in Python:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from sql_diff_explainer import compare_sql_directories
|
|
126
|
+
|
|
127
|
+
result = compare_sql_directories("models/main", "models/review")
|
|
128
|
+
print(result.to_dict())
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Git-aware pull-request comparison
|
|
132
|
+
|
|
133
|
+
Compare SQL paths Git reports as changed between a base ref and the current
|
|
134
|
+
worktree. Modified files and SQL-to-SQL renames are compared semantically;
|
|
135
|
+
added or removed SQL files are reported explicitly as high risk. Untracked
|
|
136
|
+
files are not included. The base can be a branch, tag, or commit:
|
|
137
|
+
|
|
138
|
+
```console
|
|
139
|
+
sql-diff-explainer --git-base origin/main --format json
|
|
140
|
+
sql-diff-explainer --git-base "$GITHUB_BASE_REF" --format sarif --output sql-diff.sarif
|
|
141
|
+
sql-diff-explainer --git-base origin/main --fail-on-risk high
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
For GitHub Actions, fetch the base history and use the pull request's base
|
|
145
|
+
commit. This example checks out the pull-request worktree, installs this
|
|
146
|
+
repository's source (so the CLI includes the feature), emits SARIF, and fails
|
|
147
|
+
the job on high-risk changes:
|
|
148
|
+
|
|
149
|
+
```yaml
|
|
150
|
+
name: SQL review
|
|
151
|
+
on:
|
|
152
|
+
pull_request:
|
|
153
|
+
|
|
154
|
+
jobs:
|
|
155
|
+
sql-diff:
|
|
156
|
+
runs-on: ubuntu-latest
|
|
157
|
+
steps:
|
|
158
|
+
- uses: actions/checkout@v4
|
|
159
|
+
with:
|
|
160
|
+
fetch-depth: 0
|
|
161
|
+
- uses: actions/setup-python@v5
|
|
162
|
+
with:
|
|
163
|
+
python-version: "3.12"
|
|
164
|
+
- run: python -m pip install .
|
|
165
|
+
- name: Analyze changed SQL
|
|
166
|
+
run: >-
|
|
167
|
+
sql-diff-explainer
|
|
168
|
+
--git-base "${{ github.event.pull_request.base.sha }}"
|
|
169
|
+
--format sarif
|
|
170
|
+
--output sql-diff.sarif
|
|
171
|
+
--fail-on-risk high
|
|
172
|
+
- name: Upload SQL findings
|
|
173
|
+
if: always() && hashFiles('sql-diff.sarif') != ''
|
|
174
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
175
|
+
with:
|
|
176
|
+
sarif_file: sql-diff.sarif
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
An unresolved base ref, Git failure, unreadable SQL, or invalid SQL exits with
|
|
180
|
+
status `2` and a diagnostic. Renames appear in every report; a rename does not
|
|
181
|
+
raise risk by itself, while semantic changes in its content retain their
|
|
182
|
+
normal risk level.
|
|
183
|
+
|
|
184
|
+
### SARIF and HTML reports
|
|
185
|
+
|
|
186
|
+
Emit SARIF 2.1.0 results for GitHub code scanning integrations, or generate a
|
|
187
|
+
self-contained HTML report that can be attached to a review:
|
|
188
|
+
|
|
189
|
+
```console
|
|
190
|
+
sql-diff-explainer before.sql after.sql --format sarif --output sql-diff.sarif
|
|
191
|
+
sql-diff-explainer --compare-dirs models/main models/review --format html --output review.html
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Both formats work with the existing `--fail-on-risk` threshold. SARIF maps
|
|
195
|
+
high, medium, and low risk to `error`, `warning`, and `note`; report output is
|
|
196
|
+
still written when the threshold causes exit status `1`. HTML escapes SQL text
|
|
197
|
+
and file paths. Python callers can use `to_sarif(result)` or
|
|
198
|
+
`render_html_report(result)` with either `SQLDiffResult` or
|
|
199
|
+
`SQLDirectoryDiffResult`.
|
|
200
|
+
|
|
201
|
+
## What it compares
|
|
202
|
+
|
|
203
|
+
The analyzer compares supported `SELECT` query features and reports additions,
|
|
204
|
+
removals, or changes with a low, medium, or high risk classification:
|
|
205
|
+
|
|
206
|
+
| Feature | Examples of changes |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| Projections | Selected columns and expressions |
|
|
209
|
+
| Joins | Join target, predicate, or type |
|
|
210
|
+
| Filters | `WHERE` predicates |
|
|
211
|
+
| Aggregation | Aggregate functions and `GROUP BY` expressions |
|
|
212
|
+
| Ordering and pagination | `ORDER BY`, `LIMIT`, and `OFFSET` |
|
|
213
|
+
| Duplicate handling | `DISTINCT` added or removed |
|
|
214
|
+
|
|
215
|
+
Join or filter changes can alter which rows qualify; grouping or aggregation
|
|
216
|
+
changes can change a metric's meaning or result grain; and pagination changes
|
|
217
|
+
can affect which rows downstream consumers receive.
|
|
218
|
+
|
|
219
|
+
## Use cases
|
|
220
|
+
|
|
221
|
+
- Review dbt models or warehouse SQL in a pull request.
|
|
222
|
+
- Spot join and filter changes that could alter row counts.
|
|
223
|
+
- Check grouping and aggregation changes that could shift metric grain.
|
|
224
|
+
- Add a lightweight risk threshold to a CI check.
|
|
225
|
+
- Explain generated SQL changes without treating formatting-only differences
|
|
226
|
+
as query changes.
|
|
227
|
+
|
|
228
|
+
## Scope and limitations
|
|
229
|
+
|
|
230
|
+
This is static analysis, not a SQL execution engine or correctness proof. It
|
|
231
|
+
does not connect to a database, validate schemas or permissions, estimate query
|
|
232
|
+
plans or cost, inspect table statistics or null rates, or decide whether a
|
|
233
|
+
business metric is correct. It compares supported query structure; it does not
|
|
234
|
+
analyze a multi-statement migration file or guarantee complete semantic
|
|
235
|
+
equivalence across SQL dialects. Risk levels are review signals, not
|
|
236
|
+
predictions. Pair the report with warehouse tests, representative row-count
|
|
237
|
+
checks, query-plan review, and domain-owner input.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
The package requires Python 3.10 or newer and uses a `src/` layout.
|
|
242
|
+
|
|
243
|
+
```console
|
|
244
|
+
python -m venv .venv
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Activate the environment, then install the development dependencies and run
|
|
248
|
+
the checks:
|
|
249
|
+
|
|
250
|
+
```console
|
|
251
|
+
# macOS / Linux
|
|
252
|
+
source .venv/bin/activate
|
|
253
|
+
|
|
254
|
+
# Windows PowerShell
|
|
255
|
+
.venv\Scripts\Activate.ps1
|
|
256
|
+
|
|
257
|
+
python -m pip install -e ".[dev]"
|
|
258
|
+
python -m pytest
|
|
259
|
+
ruff check .
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Contributing
|
|
263
|
+
|
|
264
|
+
Bug reports, focused improvements, and pull requests are welcome. Please
|
|
265
|
+
include or update tests for behavior changes and keep documentation aligned
|
|
266
|
+
with the supported API.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
Distributed under the [MIT License](LICENSE).
|
|
@@ -4,23 +4,34 @@ from .core import (
|
|
|
4
4
|
SQLChange,
|
|
5
5
|
SQLDiffError,
|
|
6
6
|
SQLDiffResult,
|
|
7
|
+
SQLDirectoryDiffResult,
|
|
8
|
+
SQLFileDiff,
|
|
7
9
|
SqlglotParser,
|
|
8
10
|
SQLParseError,
|
|
9
11
|
SQLParser,
|
|
10
12
|
UnsupportedSQL,
|
|
13
|
+
compare_sql_directories,
|
|
14
|
+
compare_sql_git,
|
|
11
15
|
explain_diff,
|
|
12
16
|
explain_sql_change,
|
|
13
17
|
)
|
|
18
|
+
from .reports import render_html_report, to_sarif
|
|
14
19
|
|
|
15
20
|
__all__ = [
|
|
16
21
|
"SQLChange",
|
|
22
|
+
"SQLDirectoryDiffResult",
|
|
17
23
|
"SQLDiffError",
|
|
18
24
|
"SQLDiffResult",
|
|
25
|
+
"SQLFileDiff",
|
|
19
26
|
"SQLParseError",
|
|
20
27
|
"SQLParser",
|
|
21
28
|
"SqlglotParser",
|
|
22
29
|
"UnsupportedSQL",
|
|
30
|
+
"compare_sql_directories",
|
|
31
|
+
"compare_sql_git",
|
|
23
32
|
"explain_diff",
|
|
24
33
|
"explain_sql_change",
|
|
34
|
+
"render_html_report",
|
|
35
|
+
"to_sarif",
|
|
25
36
|
]
|
|
26
|
-
__version__ = "0.1.
|
|
37
|
+
__version__ = "0.1.4"
|