sql-diff-explainer 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
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/__init__.py +31 -1
- sql_diff_explainer/analysis.py +804 -0
- sql_diff_explainer/cli.py +159 -29
- sql_diff_explainer/core.py +235 -3
- sql_diff_explainer/reports.py +215 -1
- {sql_diff_explainer-0.1.3.dist-info → sql_diff_explainer-0.1.5.dist-info}/METADATA +130 -7
- sql_diff_explainer-0.1.5.dist-info/RECORD +10 -0
- sql_diff_explainer-0.1.3.dist-info/RECORD +0 -9
- {sql_diff_explainer-0.1.3.dist-info → sql_diff_explainer-0.1.5.dist-info}/WHEEL +0 -0
- {sql_diff_explainer-0.1.3.dist-info → sql_diff_explainer-0.1.5.dist-info}/entry_points.txt +0 -0
- {sql_diff_explainer-0.1.3.dist-info → sql_diff_explainer-0.1.5.dist-info}/licenses/LICENSE +0 -0
sql_diff_explainer/reports.py
CHANGED
|
@@ -84,6 +84,117 @@ def to_sarif(
|
|
|
84
84
|
},
|
|
85
85
|
)
|
|
86
86
|
)
|
|
87
|
+
contract = sql_result.schema_contract
|
|
88
|
+
if contract and contract.get("supported"):
|
|
89
|
+
risks = {
|
|
90
|
+
"added": "medium",
|
|
91
|
+
"removed": "high",
|
|
92
|
+
"renamed": "medium",
|
|
93
|
+
"reordered": "medium",
|
|
94
|
+
"changed": "medium",
|
|
95
|
+
"type_changed": "high",
|
|
96
|
+
}
|
|
97
|
+
for item in contract.get("changes", []):
|
|
98
|
+
kind = item["kind"]
|
|
99
|
+
before = item["before"]
|
|
100
|
+
after = item["after"]
|
|
101
|
+
before_label = (
|
|
102
|
+
f"{before['name']} ({before['type'] or 'type unknown'})"
|
|
103
|
+
if before
|
|
104
|
+
else "absent"
|
|
105
|
+
)
|
|
106
|
+
after_label = (
|
|
107
|
+
f"{after['name']} ({after['type'] or 'type unknown'})"
|
|
108
|
+
if after
|
|
109
|
+
else "absent"
|
|
110
|
+
)
|
|
111
|
+
rule_id = f"sql-schema-{kind.replace('_', '-')}"
|
|
112
|
+
add_rule(
|
|
113
|
+
rule_id,
|
|
114
|
+
f"Output column {kind.replace('_', ' ')}",
|
|
115
|
+
f"SQL output column {kind.replace('_', ' ')}",
|
|
116
|
+
)
|
|
117
|
+
sarif_results.append(
|
|
118
|
+
_sarif_result(
|
|
119
|
+
rule_id=rule_id,
|
|
120
|
+
risk=risks[kind],
|
|
121
|
+
message=(
|
|
122
|
+
f"Output column {kind.replace('_', ' ')}: "
|
|
123
|
+
f"{before_label} -> {after_label}."
|
|
124
|
+
),
|
|
125
|
+
path=file_path,
|
|
126
|
+
properties={"schema_change": item},
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
elif contract:
|
|
130
|
+
rule_id = "sql-output-schema-unsupported"
|
|
131
|
+
add_rule(
|
|
132
|
+
rule_id,
|
|
133
|
+
"Output schema analysis unsupported",
|
|
134
|
+
"The output-schema contract could not be determined statically",
|
|
135
|
+
)
|
|
136
|
+
sarif_results.append(
|
|
137
|
+
_sarif_result(
|
|
138
|
+
rule_id=rule_id,
|
|
139
|
+
risk="low",
|
|
140
|
+
message=(
|
|
141
|
+
"Output-schema contract unsupported: "
|
|
142
|
+
f"{contract.get('unsupported_reason')}"
|
|
143
|
+
),
|
|
144
|
+
path=file_path,
|
|
145
|
+
properties={
|
|
146
|
+
"analysis": "output_schema",
|
|
147
|
+
"supported": False,
|
|
148
|
+
"reason": contract.get("unsupported_reason"),
|
|
149
|
+
},
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
lineage = sql_result.lineage
|
|
153
|
+
if lineage and lineage.get("supported"):
|
|
154
|
+
for item in lineage.get("changes", []):
|
|
155
|
+
output = item["after"] or item["before"]
|
|
156
|
+
before = item["before"]
|
|
157
|
+
after = item["after"]
|
|
158
|
+
before_expression = before["expression"] if before else "absent"
|
|
159
|
+
after_expression = after["expression"] if after else "absent"
|
|
160
|
+
rule_id = "sql-cte-lineage-change"
|
|
161
|
+
add_rule(
|
|
162
|
+
rule_id,
|
|
163
|
+
"CTE lineage changed",
|
|
164
|
+
"Source columns or expressions contributing to an output changed",
|
|
165
|
+
)
|
|
166
|
+
sarif_results.append(
|
|
167
|
+
_sarif_result(
|
|
168
|
+
rule_id=rule_id,
|
|
169
|
+
risk="medium",
|
|
170
|
+
message=(
|
|
171
|
+
f"CTE lineage {item['kind']} for output {output['name']}: "
|
|
172
|
+
f"{before_expression} -> {after_expression}."
|
|
173
|
+
),
|
|
174
|
+
path=file_path,
|
|
175
|
+
properties={"lineage_change": item},
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
elif lineage:
|
|
179
|
+
rule_id = "sql-cte-lineage-unsupported"
|
|
180
|
+
add_rule(
|
|
181
|
+
rule_id,
|
|
182
|
+
"CTE lineage analysis unsupported",
|
|
183
|
+
"Source-to-output lineage could not be determined statically",
|
|
184
|
+
)
|
|
185
|
+
sarif_results.append(
|
|
186
|
+
_sarif_result(
|
|
187
|
+
rule_id=rule_id,
|
|
188
|
+
risk="low",
|
|
189
|
+
message=f"CTE lineage unsupported: {lineage.get('unsupported_reason')}",
|
|
190
|
+
path=file_path,
|
|
191
|
+
properties={
|
|
192
|
+
"analysis": "cte_lineage",
|
|
193
|
+
"supported": False,
|
|
194
|
+
"reason": lineage.get("unsupported_reason"),
|
|
195
|
+
},
|
|
196
|
+
)
|
|
197
|
+
)
|
|
87
198
|
|
|
88
199
|
if isinstance(result, SQLDiffResult):
|
|
89
200
|
add_changes(result, path)
|
|
@@ -106,6 +217,18 @@ def to_sarif(
|
|
|
106
217
|
properties={"change": action},
|
|
107
218
|
)
|
|
108
219
|
)
|
|
220
|
+
if result.renamed_files:
|
|
221
|
+
add_rule("sql-file-renamed", "SQL file renamed", "SQL file renamed in the worktree")
|
|
222
|
+
for before_path, after_path in result.renamed_files:
|
|
223
|
+
sarif_results.append(
|
|
224
|
+
_sarif_result(
|
|
225
|
+
rule_id="sql-file-renamed",
|
|
226
|
+
risk="low",
|
|
227
|
+
message=f"SQL file renamed: {before_path} -> {after_path}",
|
|
228
|
+
path=after_path,
|
|
229
|
+
properties={"change": "renamed", "before": before_path, "after": after_path},
|
|
230
|
+
)
|
|
231
|
+
)
|
|
109
232
|
|
|
110
233
|
return {
|
|
111
234
|
"$schema": _SARIF_SCHEMA,
|
|
@@ -131,6 +254,7 @@ def render_html_report(
|
|
|
131
254
|
) -> str:
|
|
132
255
|
"""Render a self-contained HTML report, escaping all SQL and path content."""
|
|
133
256
|
rows: list[str] = []
|
|
257
|
+
details: list[str] = []
|
|
134
258
|
|
|
135
259
|
def add_changes(sql_result: SQLDiffResult, file_path: str) -> None:
|
|
136
260
|
for change in sql_result.changes:
|
|
@@ -143,6 +267,80 @@ def render_html_report(
|
|
|
143
267
|
f"<td><pre>{escape(change.after or '', quote=True)}</pre></td>"
|
|
144
268
|
"</tr>"
|
|
145
269
|
)
|
|
270
|
+
prefix = f"{escape(file_path, quote=True)}: " if file_path else ""
|
|
271
|
+
contract = sql_result.schema_contract
|
|
272
|
+
if contract:
|
|
273
|
+
if contract.get("supported") is False:
|
|
274
|
+
details.append(
|
|
275
|
+
"<li>"
|
|
276
|
+
f"{prefix}output-schema contract unsupported: "
|
|
277
|
+
f"{escape(str(contract.get('unsupported_reason')), quote=True)}"
|
|
278
|
+
"</li>"
|
|
279
|
+
)
|
|
280
|
+
for item in contract.get("changes", []):
|
|
281
|
+
old = item["before"]
|
|
282
|
+
new = item["after"]
|
|
283
|
+
old_label = (
|
|
284
|
+
f"{old['name']} ({old['type'] or 'type unknown'})" if old else "absent"
|
|
285
|
+
)
|
|
286
|
+
new_label = (
|
|
287
|
+
f"{new['name']} ({new['type'] or 'type unknown'})" if new else "absent"
|
|
288
|
+
)
|
|
289
|
+
details.append(
|
|
290
|
+
"<li>"
|
|
291
|
+
f"{prefix}output column "
|
|
292
|
+
f"{escape(item['kind'].replace('_', ' '), quote=True)}: "
|
|
293
|
+
f"{escape(old_label, quote=True)} → "
|
|
294
|
+
f"{escape(new_label, quote=True)}"
|
|
295
|
+
"</li>"
|
|
296
|
+
)
|
|
297
|
+
lineage = sql_result.lineage
|
|
298
|
+
if lineage:
|
|
299
|
+
if lineage.get("supported") is False:
|
|
300
|
+
details.append(
|
|
301
|
+
"<li>"
|
|
302
|
+
f"{prefix}CTE lineage unsupported: "
|
|
303
|
+
f"{escape(str(lineage.get('unsupported_reason')), quote=True)}"
|
|
304
|
+
"</li>"
|
|
305
|
+
)
|
|
306
|
+
for item in lineage.get("changes", []):
|
|
307
|
+
old = item.get("before")
|
|
308
|
+
new = item.get("after")
|
|
309
|
+
name = (new or old)["name"]
|
|
310
|
+
old_sources = ", ".join(old["sources"]) if old else "none"
|
|
311
|
+
new_sources = ", ".join(new["sources"]) if new else "none"
|
|
312
|
+
old_expression = old["expression"] if old else "absent"
|
|
313
|
+
new_expression = new["expression"] if new else "absent"
|
|
314
|
+
details.append(
|
|
315
|
+
"<li>"
|
|
316
|
+
f"{prefix}CTE lineage {escape(item['kind'], quote=True)} for "
|
|
317
|
+
f"{escape(name, quote=True)}: "
|
|
318
|
+
f"{escape(old_expression, quote=True)} [{escape(old_sources, quote=True)}] "
|
|
319
|
+
f"→ {escape(new_expression, quote=True)} "
|
|
320
|
+
f"[{escape(new_sources, quote=True)}]"
|
|
321
|
+
"</li>"
|
|
322
|
+
)
|
|
323
|
+
for check in sql_result.validation_checks:
|
|
324
|
+
details.append(
|
|
325
|
+
"<li>"
|
|
326
|
+
f"{prefix}validation ({escape(check['kind'], quote=True)}): "
|
|
327
|
+
f"{escape(check['description'], quote=True)}"
|
|
328
|
+
+ "".join(
|
|
329
|
+
f"<ul><li>{escape(item, quote=True)}</li></ul>"
|
|
330
|
+
for item in check["checklist"]
|
|
331
|
+
)
|
|
332
|
+
+ "</li>"
|
|
333
|
+
)
|
|
334
|
+
if check["sql"]:
|
|
335
|
+
details.append(
|
|
336
|
+
"<li><strong>Suggested SQL (not executed)</strong>"
|
|
337
|
+
f"<pre>{escape(check['sql'], quote=True)}</pre></li>"
|
|
338
|
+
)
|
|
339
|
+
if check["unsupported_reason"]:
|
|
340
|
+
details.append(
|
|
341
|
+
"<li>SQL not generated: "
|
|
342
|
+
f"{escape(check['unsupported_reason'], quote=True)}</li>"
|
|
343
|
+
)
|
|
146
344
|
|
|
147
345
|
if isinstance(result, SQLDiffResult):
|
|
148
346
|
risk = result.risk
|
|
@@ -169,9 +367,23 @@ def render_html_report(
|
|
|
169
367
|
"<td>SQL file added.</td><td><pre></pre></td>"
|
|
170
368
|
"<td><pre>Present after</pre></td></tr>"
|
|
171
369
|
)
|
|
370
|
+
for before_path, after_path in result.renamed_files:
|
|
371
|
+
rows.append(
|
|
372
|
+
"<tr>"
|
|
373
|
+
f"<td>{escape(after_path, quote=True)}</td>"
|
|
374
|
+
'<td class="low">LOW</td>'
|
|
375
|
+
f"<td>SQL file renamed from {escape(before_path, quote=True)}.</td>"
|
|
376
|
+
"<td><pre>Present before</pre></td>"
|
|
377
|
+
"<td><pre>Present after</pre></td></tr>"
|
|
378
|
+
)
|
|
172
379
|
|
|
173
380
|
if not rows:
|
|
174
381
|
rows.append('<tr><td colspan="5">No structural SQL changes detected.</td></tr>')
|
|
382
|
+
details_section = (
|
|
383
|
+
["<section><h2>Analysis details</h2><ul>", *details, "</ul></section>"]
|
|
384
|
+
if details
|
|
385
|
+
else []
|
|
386
|
+
)
|
|
175
387
|
return "\n".join(
|
|
176
388
|
[
|
|
177
389
|
"<!doctype html>",
|
|
@@ -192,7 +404,9 @@ def render_html_report(
|
|
|
192
404
|
"<table><thead><tr><th>File</th><th>Risk</th><th>Change</th>"
|
|
193
405
|
"<th>Before</th><th>After</th></tr></thead><tbody>",
|
|
194
406
|
*rows,
|
|
195
|
-
"</tbody></table
|
|
407
|
+
"</tbody></table>",
|
|
408
|
+
*details_section,
|
|
409
|
+
"</body></html>",
|
|
196
410
|
"",
|
|
197
411
|
]
|
|
198
412
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: sql-diff-explainer
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Explain SQL schema and migration differences
|
|
5
5
|
Author: zmodz1010
|
|
6
6
|
License: MIT
|
|
@@ -20,7 +20,7 @@ SQL Diff Explainer compares the structure of two `SELECT` queries and returns
|
|
|
20
20
|
the changes with a static risk signal, so reviews focus on query behavior rather
|
|
21
21
|
than formatting.
|
|
22
22
|
|
|
23
|
-
[](https://pypi.org/project/sql-diff-explainer/)
|
|
23
|
+
[](https://pypi.org/project/sql-diff-explainer/)
|
|
24
24
|
[](https://pypi.org/project/sql-diff-explainer/)
|
|
25
25
|
[](LICENSE)
|
|
26
26
|
|
|
@@ -29,7 +29,9 @@ than formatting.
|
|
|
29
29
|
- [Quick start](#quick-start)
|
|
30
30
|
- [Command-line usage](#command-line-usage)
|
|
31
31
|
- [Directory comparison](#directory-comparison)
|
|
32
|
+
- [Git-aware pull-request comparison](#git-aware-pull-request-comparison)
|
|
32
33
|
- [SARIF and HTML reports](#sarif-and-html-reports)
|
|
34
|
+
- [Additional SQL QA analyses](#additional-sql-qa-analyses)
|
|
33
35
|
- [What it compares](#what-it-compares)
|
|
34
36
|
- [Use cases](#use-cases)
|
|
35
37
|
- [Scope and limitations](#scope-and-limitations)
|
|
@@ -113,8 +115,15 @@ Try the runnable before/after pairs in [`examples/`](examples/):
|
|
|
113
115
|
sql-diff-explainer examples/filter_before.sql examples/filter_after.sql --format json
|
|
114
116
|
sql-diff-explainer examples/aggregation_before.sql examples/aggregation_after.sql
|
|
115
117
|
sql-diff-explainer examples/distinct_before.sql examples/distinct_after.sql
|
|
118
|
+
sql-diff-explainer examples/cte_lineage_before.sql examples/cte_lineage_after.sql --format json
|
|
119
|
+
sql-diff-explainer examples/output_contract_before.sql examples/output_contract_after.sql --format json
|
|
120
|
+
sql-diff-explainer --analyze-migration examples/migration.sql --format json
|
|
116
121
|
```
|
|
117
122
|
|
|
123
|
+
`--analyze-migration FILE` accepts multi-statement migration scripts and emits
|
|
124
|
+
text or JSON. It supports common `ALTER TABLE` column operations; SARIF and
|
|
125
|
+
HTML formats are not available for this separate migration mode.
|
|
126
|
+
|
|
118
127
|
### Directory comparison
|
|
119
128
|
|
|
120
129
|
Compare two directory trees recursively. SQL files are paired by their exact
|
|
@@ -141,6 +150,59 @@ result = compare_sql_directories("models/main", "models/review")
|
|
|
141
150
|
print(result.to_dict())
|
|
142
151
|
```
|
|
143
152
|
|
|
153
|
+
### Git-aware pull-request comparison
|
|
154
|
+
|
|
155
|
+
Compare SQL paths Git reports as changed between a base ref and the current
|
|
156
|
+
worktree. Modified files and SQL-to-SQL renames are compared semantically;
|
|
157
|
+
added or removed SQL files are reported explicitly as high risk. Untracked
|
|
158
|
+
files are not included. The base can be a branch, tag, or commit:
|
|
159
|
+
|
|
160
|
+
```console
|
|
161
|
+
sql-diff-explainer --git-base origin/main --format json
|
|
162
|
+
sql-diff-explainer --git-base "$GITHUB_BASE_REF" --format sarif --output sql-diff.sarif
|
|
163
|
+
sql-diff-explainer --git-base origin/main --fail-on-risk high
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
For GitHub Actions, fetch the base history and use the pull request's base
|
|
167
|
+
commit. This example checks out the pull-request worktree, installs this
|
|
168
|
+
repository's source (so the CLI includes the feature), emits SARIF, and fails
|
|
169
|
+
the job on high-risk changes:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
name: SQL review
|
|
173
|
+
on:
|
|
174
|
+
pull_request:
|
|
175
|
+
|
|
176
|
+
jobs:
|
|
177
|
+
sql-diff:
|
|
178
|
+
runs-on: ubuntu-latest
|
|
179
|
+
steps:
|
|
180
|
+
- uses: actions/checkout@v4
|
|
181
|
+
with:
|
|
182
|
+
fetch-depth: 0
|
|
183
|
+
- uses: actions/setup-python@v5
|
|
184
|
+
with:
|
|
185
|
+
python-version: "3.12"
|
|
186
|
+
- run: python -m pip install .
|
|
187
|
+
- name: Analyze changed SQL
|
|
188
|
+
run: >-
|
|
189
|
+
sql-diff-explainer
|
|
190
|
+
--git-base "${{ github.event.pull_request.base.sha }}"
|
|
191
|
+
--format sarif
|
|
192
|
+
--output sql-diff.sarif
|
|
193
|
+
--fail-on-risk high
|
|
194
|
+
- name: Upload SQL findings
|
|
195
|
+
if: always() && hashFiles('sql-diff.sarif') != ''
|
|
196
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
197
|
+
with:
|
|
198
|
+
sarif_file: sql-diff.sarif
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
An unresolved base ref, Git failure, unreadable SQL, or invalid SQL exits with
|
|
202
|
+
status `2` and a diagnostic. Renames appear in every report; a rename does not
|
|
203
|
+
raise risk by itself, while semantic changes in its content retain their
|
|
204
|
+
normal risk level.
|
|
205
|
+
|
|
144
206
|
### SARIF and HTML reports
|
|
145
207
|
|
|
146
208
|
Emit SARIF 2.1.0 results for GitHub code scanning integrations, or generate a
|
|
@@ -158,6 +220,63 @@ and file paths. Python callers can use `to_sarif(result)` or
|
|
|
158
220
|
`render_html_report(result)` with either `SQLDiffResult` or
|
|
159
221
|
`SQLDirectoryDiffResult`.
|
|
160
222
|
|
|
223
|
+
## Additional SQL QA analyses
|
|
224
|
+
|
|
225
|
+
Query comparisons include three static QA views in `SQLDiffResult` and JSON
|
|
226
|
+
output:
|
|
227
|
+
|
|
228
|
+
- **CTE lineage** (`lineage`) traces explicit output expressions through
|
|
229
|
+
non-recursive CTE chains to their source columns. Changed sources or
|
|
230
|
+
expressions are also included in `changes` and the risk summary. Lineage
|
|
231
|
+
requires explicit projections, aliases for calculated expressions, and
|
|
232
|
+
direct table/CTE sources; recursive CTEs, wildcard projections, nested
|
|
233
|
+
subqueries, CTE column alias lists, and ambiguous unqualified columns are
|
|
234
|
+
reported as unsupported.
|
|
235
|
+
- **Output-schema contracts** (`schema_contract`) report added, removed,
|
|
236
|
+
renamed, reordered, changed, and supported type-changed output columns.
|
|
237
|
+
Types are inferred only for explicit casts and aliased literals; column types
|
|
238
|
+
that require a database catalog remain unknown. Wildcard projections, unnamed
|
|
239
|
+
expressions, and set operations are reported as unsupported.
|
|
240
|
+
- **Validation suggestions** (`validation_checks`) provide a checklist and,
|
|
241
|
+
when the before/after output columns are compatible, SQL for unmatched-row
|
|
242
|
+
and duplicate checks. The unmatched-row query uses `EXCEPT`; adapt it for
|
|
243
|
+
dialects that use different set-difference syntax. These checks are
|
|
244
|
+
suggestions only: SQL Diff Explainer never connects to a database or
|
|
245
|
+
executes generated SQL.
|
|
246
|
+
|
|
247
|
+
Use the analysis helpers directly when needed:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
from sql_diff_explainer import (
|
|
251
|
+
analyze_migration,
|
|
252
|
+
compare_output_schema,
|
|
253
|
+
trace_cte_lineage,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
before_sql = """
|
|
257
|
+
WITH totals AS (SELECT customer_id, amount FROM sales)
|
|
258
|
+
SELECT customer_id, amount FROM totals
|
|
259
|
+
"""
|
|
260
|
+
after_sql = """
|
|
261
|
+
WITH totals AS (SELECT customer_id, net_amount AS amount FROM sales)
|
|
262
|
+
SELECT customer_id, amount FROM totals
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
lineage = trace_cte_lineage(before_sql, after_sql)
|
|
266
|
+
contract = compare_output_schema(before_sql, after_sql)
|
|
267
|
+
migration = analyze_migration(
|
|
268
|
+
"ALTER TABLE users ADD COLUMN email TEXT;"
|
|
269
|
+
"ALTER TABLE users RENAME COLUMN old_email TO email_address;"
|
|
270
|
+
)
|
|
271
|
+
print(lineage.to_dict(), contract.to_dict(), migration.to_dict())
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Migration analysis recognizes `ADD COLUMN`, `DROP COLUMN`, `RENAME COLUMN`,
|
|
275
|
+
`ALTER COLUMN ... TYPE`, and `ALTER COLUMN ... SET/DROP NOT NULL`. Other
|
|
276
|
+
statements and ALTER actions are listed as unsupported rather than being
|
|
277
|
+
silently treated as analyzed. Use `--analyze-migration` for a file or
|
|
278
|
+
`analyze_migration(sql)` from Python.
|
|
279
|
+
|
|
161
280
|
## What it compares
|
|
162
281
|
|
|
163
282
|
The analyzer compares supported `SELECT` query features and reports additions,
|
|
@@ -171,6 +290,7 @@ removals, or changes with a low, medium, or high risk classification:
|
|
|
171
290
|
| Aggregation | Aggregate functions and `GROUP BY` expressions |
|
|
172
291
|
| Ordering and pagination | `ORDER BY`, `LIMIT`, and `OFFSET` |
|
|
173
292
|
| Duplicate handling | `DISTINCT` added or removed |
|
|
293
|
+
| Output schema | Result-column additions, removals, renames, ordering, and known type changes |
|
|
174
294
|
|
|
175
295
|
Join or filter changes can alter which rows qualify; grouping or aggregation
|
|
176
296
|
changes can change a metric's meaning or result grain; and pagination changes
|
|
@@ -190,11 +310,14 @@ can affect which rows downstream consumers receive.
|
|
|
190
310
|
This is static analysis, not a SQL execution engine or correctness proof. It
|
|
191
311
|
does not connect to a database, validate schemas or permissions, estimate query
|
|
192
312
|
plans or cost, inspect table statistics or null rates, or decide whether a
|
|
193
|
-
business metric is correct.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
313
|
+
business metric is correct. Output-column types that require database metadata
|
|
314
|
+
are unknown. CTE lineage and query validation SQL support only the documented
|
|
315
|
+
static cases, and migration analysis recognizes only the listed `ALTER TABLE`
|
|
316
|
+
operations. Generated validation SQL is not executed and may need dialect
|
|
317
|
+
adaptation. The analyzer does not guarantee complete semantic equivalence
|
|
318
|
+
across SQL dialects. Risk levels are review signals, not predictions. Pair the
|
|
319
|
+
report with warehouse tests, representative row-count checks, query-plan
|
|
320
|
+
review, and domain-owner input.
|
|
198
321
|
|
|
199
322
|
## Development
|
|
200
323
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
sql_diff_explainer/__init__.py,sha256=aAC9OzousRZ9LohX-3rihFzxbsJIAEL-3u1mYw1mtPk,1479
|
|
2
|
+
sql_diff_explainer/analysis.py,sha256=lpslLkUcszmlyK64-TWwhmuq1ywLpdM13nGdMglIfCU,29734
|
|
3
|
+
sql_diff_explainer/cli.py,sha256=M95AFxK1yb_0DQNb1HLLmczq6DrWKKnS1rHuA6n1xRc,11292
|
|
4
|
+
sql_diff_explainer/core.py,sha256=ZUtSCyocITV6FshwFjUjqu2oVTi6OJq5NsiT6IpSmAs,26854
|
|
5
|
+
sql_diff_explainer/reports.py,sha256=Nz8cttmNJ0B0awIZDaCX5L2qFD4wcEZH36IsHv8_OJI,16573
|
|
6
|
+
sql_diff_explainer-0.1.5.dist-info/METADATA,sha256=ype-3pwxVnNha6LbRuFJpcfKv5dwdgUjKhPaSlJ7zUU,12967
|
|
7
|
+
sql_diff_explainer-0.1.5.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
8
|
+
sql_diff_explainer-0.1.5.dist-info/entry_points.txt,sha256=Pu2gG46es25JExzGO58plyLEsFedeCUM-TQnUeLpOlI,67
|
|
9
|
+
sql_diff_explainer-0.1.5.dist-info/licenses/LICENSE,sha256=OJ2eEuGmpMxVb7jSmuVgeDcVHCEOMBkZgLgTlnK5mQQ,1087
|
|
10
|
+
sql_diff_explainer-0.1.5.dist-info/RECORD,,
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
sql_diff_explainer/__init__.py,sha256=_Thcgi2aYlJnnAf1beLKPaVHpbEK9doHILFDliYjUsE,743
|
|
2
|
-
sql_diff_explainer/cli.py,sha256=W654_Apfe4o9nWAGlPDAvJs6nmSQZuu34RIlV0W4spc,4866
|
|
3
|
-
sql_diff_explainer/core.py,sha256=3Qx7vkPZUcKfN_u55ID0Ebr30zHDheE75xggtiYa904,17389
|
|
4
|
-
sql_diff_explainer/reports.py,sha256=jIY9lEa3xnj6Q1xojzB3N0S9t-AUAWzNlvRc4rFDQBo,7228
|
|
5
|
-
sql_diff_explainer-0.1.3.dist-info/METADATA,sha256=GwXOieoenCVZOQ1bkHaBVPmjF_VkR-bs4rZZJWTU06M,7795
|
|
6
|
-
sql_diff_explainer-0.1.3.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
-
sql_diff_explainer-0.1.3.dist-info/entry_points.txt,sha256=Pu2gG46es25JExzGO58plyLEsFedeCUM-TQnUeLpOlI,67
|
|
8
|
-
sql_diff_explainer-0.1.3.dist-info/licenses/LICENSE,sha256=OJ2eEuGmpMxVb7jSmuVgeDcVHCEOMBkZgLgTlnK5mQQ,1087
|
|
9
|
-
sql_diff_explainer-0.1.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|