pysqlmut 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.
- pysqlmut-0.1.0/LICENSE +21 -0
- pysqlmut-0.1.0/PKG-INFO +244 -0
- pysqlmut-0.1.0/README.md +213 -0
- pysqlmut-0.1.0/pyproject.toml +106 -0
- pysqlmut-0.1.0/pyproject.toml.orig +79 -0
- pysqlmut-0.1.0/src/pysqlmut/__init__.py +1 -0
- pysqlmut-0.1.0/src/pysqlmut/accepted.py +40 -0
- pysqlmut-0.1.0/src/pysqlmut/cli.py +347 -0
- pysqlmut-0.1.0/src/pysqlmut/config.py +149 -0
- pysqlmut-0.1.0/src/pysqlmut/mutants.py +186 -0
- pysqlmut-0.1.0/src/pysqlmut/operators.py +575 -0
- pysqlmut-0.1.0/src/pysqlmut/pytest_worker.py +143 -0
- pysqlmut-0.1.0/src/pysqlmut/report.py +78 -0
- pysqlmut-0.1.0/src/pysqlmut/runner.py +426 -0
- pysqlmut-0.1.0/src/pysqlmut/source.py +139 -0
pysqlmut-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Aydinbas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pysqlmut-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pysqlmut
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mutation testing for SQL: change SQL files in small, valid ways and report the changes your tests miss.
|
|
5
|
+
Keywords: sql,mutation testing,testing,sqlglot,pytest,duckdb,data engineering
|
|
6
|
+
Author: Michael Aydinbas
|
|
7
|
+
Author-email: Michael Aydinbas <michael.aydinbas@pm.me>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Programming Language :: SQL
|
|
20
|
+
Classifier: Topic :: Database
|
|
21
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Dist: sqlglot>=30.18,<31
|
|
24
|
+
Requires-Dist: typer>=0.27.2
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Project-URL: Homepage, https://github.com/pmayd/pysqlmut
|
|
27
|
+
Project-URL: Repository, https://github.com/pmayd/pysqlmut
|
|
28
|
+
Project-URL: Issues, https://github.com/pmayd/pysqlmut/issues
|
|
29
|
+
Project-URL: Changelog, https://github.com/pmayd/pysqlmut/blob/main/CHANGELOG.md
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# pysqlmut
|
|
33
|
+
|
|
34
|
+
[](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Mutation testing for SQL.
|
|
38
|
+
|
|
39
|
+
pysqlmut changes your SQL files in small, valid ways (a flipped comparison, a swapped aggregate, a dropped
|
|
40
|
+
join condition, a column mixed up with a similar one), runs your tests against every change, and reports
|
|
41
|
+
the changes no test noticed. Each surviving change points at a rule in your SQL that your tests do not pin
|
|
42
|
+
down.
|
|
43
|
+
|
|
44
|
+
It is made for SQL that is tested by running it, for example on DuckDB inside pytest. Tests that compare
|
|
45
|
+
SQL text cannot catch these changes.
|
|
46
|
+
|
|
47
|
+
**New here?** [GETTING_STARTED.md](GETTING_STARTED.md) walks through a small example from the first run to
|
|
48
|
+
closing the gaps it finds. [ROADMAP.md](ROADMAP.md) lists what is missing and what may come next.
|
|
49
|
+
|
|
50
|
+
## Status
|
|
51
|
+
|
|
52
|
+
Early development. The command line, the settings and the report format may still change before 1.0.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
uv tool install pysqlmut # or: pipx install pysqlmut
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- pysqlmut needs Python 3.12 or newer and runs on Linux, macOS and Windows through WSL. Installed as a tool, it
|
|
61
|
+
lives in an environment of its own, apart from your project.
|
|
62
|
+
- Your tests keep running in your project's own environment. For the pytest runner, that environment needs
|
|
63
|
+
pytest 8.1 or newer on Python 3.9 or newer; see [Choosing a runner](#choosing-a-runner).
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd your-project
|
|
69
|
+
|
|
70
|
+
# See which changes pysqlmut would make, without running any test.
|
|
71
|
+
pysqlmut generate sql/orders.sql --dialect duckdb --show
|
|
72
|
+
|
|
73
|
+
# Run the tests against every change: 8 copies of the project, each with a pytest process that runs
|
|
74
|
+
# only the tests that read the changed file.
|
|
75
|
+
pysqlmut run sql/orders.sql --dialect duckdb --pytest "uv run python" --tests tests/sql \
|
|
76
|
+
--workers 8 --report pysqlmut-report.json
|
|
77
|
+
|
|
78
|
+
# Review the survivors later, grouped by the code they changed.
|
|
79
|
+
pysqlmut report pysqlmut-report.json
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
A group of survivors looks like this:
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
3x comparison (= -> <>): x.x = 'S'
|
|
86
|
+
e.g. sql/orders.sql:14
|
|
87
|
+
- WHERE o.status = 'paid'
|
|
88
|
+
+ WHERE o.status <> 'paid'
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
The same rule written in several places, with different names and values, is one group, so a repeated gap
|
|
92
|
+
shows up once.
|
|
93
|
+
|
|
94
|
+
## How it works
|
|
95
|
+
|
|
96
|
+
**Generating mutants.** sqlglot parses each statement. Each operator looks at one expression and proposes a
|
|
97
|
+
text patch together with the change it means for the parse tree. A mutant is kept only when the patched
|
|
98
|
+
statement parses to exactly that changed tree, so a mutant never changes more than it claims, and the rest of
|
|
99
|
+
the file stays as it is, comments, formatting and line endings included. Statements sqlglot cannot parse are
|
|
100
|
+
skipped. For the same SQL and settings, the mutants and their order are always the same.
|
|
101
|
+
|
|
102
|
+
**Running tests.** Every worker gets its own copy of the project, which shares the project's virtual
|
|
103
|
+
environment; your working tree is never changed. The tests run with one of two runners, described next.
|
|
104
|
+
|
|
105
|
+
Ctrl-C or a timeout ends the test processes a run started. A run stops before testing any mutant when the
|
|
106
|
+
tests fail on the unchanged project, or when they read the project itself instead of the copy that holds the
|
|
107
|
+
mutant, since no result would mean anything then.
|
|
108
|
+
|
|
109
|
+
## Choosing a runner
|
|
110
|
+
|
|
111
|
+
pysqlmut has to run your tests once for every mutant, so how it runs them decides how long a run takes.
|
|
112
|
+
|
|
113
|
+
**`--command "..."` runs any test command.** Give it the command you would type, for example
|
|
114
|
+
`--command "uv run pytest tests/sql"`, `--command "python -m unittest"` or `--command "make test"`. For every
|
|
115
|
+
mutant, pysqlmut starts that command in a new process and reads its exit code. This works with every test
|
|
116
|
+
tool, but each mutant pays for starting Python, importing your dependencies and running the whole suite.
|
|
117
|
+
|
|
118
|
+
Use it to try pysqlmut, for test tools other than pytest, and for tests that read the SQL in another process,
|
|
119
|
+
such as dbt, a database command line tool or pytest-xdist workers.
|
|
120
|
+
|
|
121
|
+
**`--pytest PYTHON` runs pytest inside your project's Python.** Give it the command that starts the Python of
|
|
122
|
+
your project's environment, the one with pytest and your project's dependencies installed: `uv run python`,
|
|
123
|
+
`poetry run python`, or a path such as `.venv/bin/python`. pysqlmut needs it because it is installed apart from
|
|
124
|
+
your project, so its own Python cannot import your code or your test dependencies.
|
|
125
|
+
|
|
126
|
+
pysqlmut starts pytest in that Python once per worker and keeps it running. A first run records which test
|
|
127
|
+
reads which SQL file, directly or through a fixture. For each mutant, pysqlmut then runs only the tests that
|
|
128
|
+
read the changed file, in a fork of the running process that imports your project's modules again, so
|
|
129
|
+
libraries such as DuckDB or pandas are not imported again for every mutant. When a file is read while modules
|
|
130
|
+
are imported, every test runs for its mutants.
|
|
131
|
+
|
|
132
|
+
Use it when your tests are pytest tests; pytest also runs `unittest.TestCase` tests. On a large suite where
|
|
133
|
+
each SQL file is read by only a few tests, it is much faster than `--command`.
|
|
134
|
+
|
|
135
|
+
## Results
|
|
136
|
+
|
|
137
|
+
| Status | Meaning |
|
|
138
|
+
|---|---|
|
|
139
|
+
| caught | a test failed |
|
|
140
|
+
| survived | every test passed although the SQL changed |
|
|
141
|
+
| not covered | no test was seen reading the file, so nothing ran |
|
|
142
|
+
| accepted | a reviewed survivor, not run again |
|
|
143
|
+
| timeout | the tests did not finish in time |
|
|
144
|
+
| error | the tests could not run, for example pytest collected no tests |
|
|
145
|
+
|
|
146
|
+
*Survivors* are the mutants that survived or were not covered.
|
|
147
|
+
|
|
148
|
+
| Exit code | When |
|
|
149
|
+
|---|---|
|
|
150
|
+
| 0 | every mutant was caught or accepted |
|
|
151
|
+
| 1 | mutants survived, were not covered, timed out or ended in an error |
|
|
152
|
+
| 2 | a usage or settings error |
|
|
153
|
+
| 3 | the tests fail on the unchanged project, or read the project instead of its copy |
|
|
154
|
+
| 4 | pysqlmut itself failed; please report it |
|
|
155
|
+
| 130 | the run was interrupted |
|
|
156
|
+
|
|
157
|
+
## Operators
|
|
158
|
+
|
|
159
|
+
| Operator | Change |
|
|
160
|
+
|---|---|
|
|
161
|
+
| `comparison` | `=` and `<>` swap; `<` and `<=`, `>` and `>=` move the boundary |
|
|
162
|
+
| `logical` | `AND` and `OR` swap |
|
|
163
|
+
| `drop-condition` | one side of an `AND` is dropped |
|
|
164
|
+
| `is-null` | `IS NULL` and `IS NOT NULL` swap |
|
|
165
|
+
| `aggregate` | `SUM` becomes `MAX`, `MAX` becomes `MIN`, `MIN` becomes `MAX`, `AVG` becomes `SUM` |
|
|
166
|
+
| `coalesce` | only the first argument is kept, or the first two swap |
|
|
167
|
+
| `order` | `ASC` and `DESC` swap |
|
|
168
|
+
| `union` | `UNION` and `UNION ALL` swap |
|
|
169
|
+
| `join-type` | `LEFT JOIN` and `INNER JOIN` swap |
|
|
170
|
+
| `arithmetic` | `+` and `-`, `*` and `/` swap; a unary minus is dropped |
|
|
171
|
+
| `literal` | an integer grows by one |
|
|
172
|
+
| `distinct` | `DISTINCT` is dropped from `SELECT` and `COUNT` |
|
|
173
|
+
| `case` | the `ELSE` value becomes `NULL`, or one `WHEN` branch is dropped |
|
|
174
|
+
| `string-literal` | a string gets a suffix |
|
|
175
|
+
| `column` | a column becomes the most similarly named other column of the same table |
|
|
176
|
+
|
|
177
|
+
A change that provably cannot alter any result is left out: `UNION ALL` and `UNION` return the same rows when
|
|
178
|
+
every branch returns unique rows and each pair of branches differs in a literal column.
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
Settings live in the tested project's `pyproject.toml`; command line options override them. Paths in the
|
|
183
|
+
settings are relative to the project, and patterns are globs, where `*` does not cross a directory.
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
[tool.pysqlmut]
|
|
187
|
+
dialect = "duckdb"
|
|
188
|
+
files = ["sql/**/*.sql"]
|
|
189
|
+
exclude-operators = ["union"]
|
|
190
|
+
accepted = "pysqlmut-accepted.json"
|
|
191
|
+
workers = 8 # processes that generate mutants, then parallel test runs
|
|
192
|
+
timeout = 180 # seconds per mutant
|
|
193
|
+
|
|
194
|
+
# Long-lived pytest workers that run only the tests reading the mutated file.
|
|
195
|
+
[tool.pysqlmut.pytest]
|
|
196
|
+
python = "uv run python"
|
|
197
|
+
tests = ["tests/sql"]
|
|
198
|
+
args = ["-q", "-x"]
|
|
199
|
+
|
|
200
|
+
[[tool.pysqlmut.file]]
|
|
201
|
+
pattern = "sql/labels.sql"
|
|
202
|
+
exclude-operators = ["string-literal"]
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Instead of `[tool.pysqlmut.pytest]`, `command = "..."` runs any test command, in a new process per mutant.
|
|
206
|
+
With the settings in place, `pysqlmut run` needs no arguments. Unknown settings and values of the wrong type
|
|
207
|
+
are reported before anything runs.
|
|
208
|
+
|
|
209
|
+
## Reviewing survivors
|
|
210
|
+
|
|
211
|
+
- `-- pysqlmut: skip` at the end of a line excludes that line; `-- pysqlmut: off` and `-- pysqlmut: on`
|
|
212
|
+
exclude a block, including changes that would reach into it.
|
|
213
|
+
- `pysqlmut accept pysqlmut-report.json` records the survivors of a report as reviewed, and later runs neither
|
|
214
|
+
run nor report them. A survivor is identified by its file, operator, description and the line before and
|
|
215
|
+
after the change, so edits elsewhere in the file keep it accepted. `--operators` accepts only some
|
|
216
|
+
operators. Commit the accepted file with the project.
|
|
217
|
+
- `pysqlmut generate` (also available as `pysqlmut list`) shows every mutant and why candidates were
|
|
218
|
+
rejected.
|
|
219
|
+
|
|
220
|
+
## Limitations
|
|
221
|
+
|
|
222
|
+
- With `--pytest`, a file read by a subprocess, a pytest-xdist worker or native code counts as not covered.
|
|
223
|
+
Use `--command` for such projects.
|
|
224
|
+
- Some survivors cannot change a result for any data your SQL can see, for example `MAX` to `MIN` on a value
|
|
225
|
+
that is the same in every row of its group. pysqlmut only removes such changes when it can prove it from the
|
|
226
|
+
SQL alone; accept the rest after review.
|
|
227
|
+
- `string-literal` and `case` produce many survivors on label and mapping SQL. Exclude them per file when their
|
|
228
|
+
survivors are not useful.
|
|
229
|
+
- Jinja templates, scripting blocks, procedures and other statements sqlglot cannot parse are not mutated.
|
|
230
|
+
- Windows is supported only through WSL.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
just setup # install dependencies and pre-commit hooks
|
|
236
|
+
just check # lint, type check, test
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`AGENTS.md` describes the design rules and what makes a test worth keeping. `RELEASING.md` describes how
|
|
240
|
+
versions are published, `SECURITY.md` how to report a vulnerability, and `CHANGELOG.md` lists the changes.
|
|
241
|
+
|
|
242
|
+
## License
|
|
243
|
+
|
|
244
|
+
MIT
|
pysqlmut-0.1.0/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# pysqlmut
|
|
2
|
+
|
|
3
|
+
[](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
|
|
6
|
+
Mutation testing for SQL.
|
|
7
|
+
|
|
8
|
+
pysqlmut changes your SQL files in small, valid ways (a flipped comparison, a swapped aggregate, a dropped
|
|
9
|
+
join condition, a column mixed up with a similar one), runs your tests against every change, and reports
|
|
10
|
+
the changes no test noticed. Each surviving change points at a rule in your SQL that your tests do not pin
|
|
11
|
+
down.
|
|
12
|
+
|
|
13
|
+
It is made for SQL that is tested by running it, for example on DuckDB inside pytest. Tests that compare
|
|
14
|
+
SQL text cannot catch these changes.
|
|
15
|
+
|
|
16
|
+
**New here?** [GETTING_STARTED.md](GETTING_STARTED.md) walks through a small example from the first run to
|
|
17
|
+
closing the gaps it finds. [ROADMAP.md](ROADMAP.md) lists what is missing and what may come next.
|
|
18
|
+
|
|
19
|
+
## Status
|
|
20
|
+
|
|
21
|
+
Early development. The command line, the settings and the report format may still change before 1.0.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
uv tool install pysqlmut # or: pipx install pysqlmut
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- pysqlmut needs Python 3.12 or newer and runs on Linux, macOS and Windows through WSL. Installed as a tool, it
|
|
30
|
+
lives in an environment of its own, apart from your project.
|
|
31
|
+
- Your tests keep running in your project's own environment. For the pytest runner, that environment needs
|
|
32
|
+
pytest 8.1 or newer on Python 3.9 or newer; see [Choosing a runner](#choosing-a-runner).
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cd your-project
|
|
38
|
+
|
|
39
|
+
# See which changes pysqlmut would make, without running any test.
|
|
40
|
+
pysqlmut generate sql/orders.sql --dialect duckdb --show
|
|
41
|
+
|
|
42
|
+
# Run the tests against every change: 8 copies of the project, each with a pytest process that runs
|
|
43
|
+
# only the tests that read the changed file.
|
|
44
|
+
pysqlmut run sql/orders.sql --dialect duckdb --pytest "uv run python" --tests tests/sql \
|
|
45
|
+
--workers 8 --report pysqlmut-report.json
|
|
46
|
+
|
|
47
|
+
# Review the survivors later, grouped by the code they changed.
|
|
48
|
+
pysqlmut report pysqlmut-report.json
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
A group of survivors looks like this:
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
3x comparison (= -> <>): x.x = 'S'
|
|
55
|
+
e.g. sql/orders.sql:14
|
|
56
|
+
- WHERE o.status = 'paid'
|
|
57
|
+
+ WHERE o.status <> 'paid'
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The same rule written in several places, with different names and values, is one group, so a repeated gap
|
|
61
|
+
shows up once.
|
|
62
|
+
|
|
63
|
+
## How it works
|
|
64
|
+
|
|
65
|
+
**Generating mutants.** sqlglot parses each statement. Each operator looks at one expression and proposes a
|
|
66
|
+
text patch together with the change it means for the parse tree. A mutant is kept only when the patched
|
|
67
|
+
statement parses to exactly that changed tree, so a mutant never changes more than it claims, and the rest of
|
|
68
|
+
the file stays as it is, comments, formatting and line endings included. Statements sqlglot cannot parse are
|
|
69
|
+
skipped. For the same SQL and settings, the mutants and their order are always the same.
|
|
70
|
+
|
|
71
|
+
**Running tests.** Every worker gets its own copy of the project, which shares the project's virtual
|
|
72
|
+
environment; your working tree is never changed. The tests run with one of two runners, described next.
|
|
73
|
+
|
|
74
|
+
Ctrl-C or a timeout ends the test processes a run started. A run stops before testing any mutant when the
|
|
75
|
+
tests fail on the unchanged project, or when they read the project itself instead of the copy that holds the
|
|
76
|
+
mutant, since no result would mean anything then.
|
|
77
|
+
|
|
78
|
+
## Choosing a runner
|
|
79
|
+
|
|
80
|
+
pysqlmut has to run your tests once for every mutant, so how it runs them decides how long a run takes.
|
|
81
|
+
|
|
82
|
+
**`--command "..."` runs any test command.** Give it the command you would type, for example
|
|
83
|
+
`--command "uv run pytest tests/sql"`, `--command "python -m unittest"` or `--command "make test"`. For every
|
|
84
|
+
mutant, pysqlmut starts that command in a new process and reads its exit code. This works with every test
|
|
85
|
+
tool, but each mutant pays for starting Python, importing your dependencies and running the whole suite.
|
|
86
|
+
|
|
87
|
+
Use it to try pysqlmut, for test tools other than pytest, and for tests that read the SQL in another process,
|
|
88
|
+
such as dbt, a database command line tool or pytest-xdist workers.
|
|
89
|
+
|
|
90
|
+
**`--pytest PYTHON` runs pytest inside your project's Python.** Give it the command that starts the Python of
|
|
91
|
+
your project's environment, the one with pytest and your project's dependencies installed: `uv run python`,
|
|
92
|
+
`poetry run python`, or a path such as `.venv/bin/python`. pysqlmut needs it because it is installed apart from
|
|
93
|
+
your project, so its own Python cannot import your code or your test dependencies.
|
|
94
|
+
|
|
95
|
+
pysqlmut starts pytest in that Python once per worker and keeps it running. A first run records which test
|
|
96
|
+
reads which SQL file, directly or through a fixture. For each mutant, pysqlmut then runs only the tests that
|
|
97
|
+
read the changed file, in a fork of the running process that imports your project's modules again, so
|
|
98
|
+
libraries such as DuckDB or pandas are not imported again for every mutant. When a file is read while modules
|
|
99
|
+
are imported, every test runs for its mutants.
|
|
100
|
+
|
|
101
|
+
Use it when your tests are pytest tests; pytest also runs `unittest.TestCase` tests. On a large suite where
|
|
102
|
+
each SQL file is read by only a few tests, it is much faster than `--command`.
|
|
103
|
+
|
|
104
|
+
## Results
|
|
105
|
+
|
|
106
|
+
| Status | Meaning |
|
|
107
|
+
|---|---|
|
|
108
|
+
| caught | a test failed |
|
|
109
|
+
| survived | every test passed although the SQL changed |
|
|
110
|
+
| not covered | no test was seen reading the file, so nothing ran |
|
|
111
|
+
| accepted | a reviewed survivor, not run again |
|
|
112
|
+
| timeout | the tests did not finish in time |
|
|
113
|
+
| error | the tests could not run, for example pytest collected no tests |
|
|
114
|
+
|
|
115
|
+
*Survivors* are the mutants that survived or were not covered.
|
|
116
|
+
|
|
117
|
+
| Exit code | When |
|
|
118
|
+
|---|---|
|
|
119
|
+
| 0 | every mutant was caught or accepted |
|
|
120
|
+
| 1 | mutants survived, were not covered, timed out or ended in an error |
|
|
121
|
+
| 2 | a usage or settings error |
|
|
122
|
+
| 3 | the tests fail on the unchanged project, or read the project instead of its copy |
|
|
123
|
+
| 4 | pysqlmut itself failed; please report it |
|
|
124
|
+
| 130 | the run was interrupted |
|
|
125
|
+
|
|
126
|
+
## Operators
|
|
127
|
+
|
|
128
|
+
| Operator | Change |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `comparison` | `=` and `<>` swap; `<` and `<=`, `>` and `>=` move the boundary |
|
|
131
|
+
| `logical` | `AND` and `OR` swap |
|
|
132
|
+
| `drop-condition` | one side of an `AND` is dropped |
|
|
133
|
+
| `is-null` | `IS NULL` and `IS NOT NULL` swap |
|
|
134
|
+
| `aggregate` | `SUM` becomes `MAX`, `MAX` becomes `MIN`, `MIN` becomes `MAX`, `AVG` becomes `SUM` |
|
|
135
|
+
| `coalesce` | only the first argument is kept, or the first two swap |
|
|
136
|
+
| `order` | `ASC` and `DESC` swap |
|
|
137
|
+
| `union` | `UNION` and `UNION ALL` swap |
|
|
138
|
+
| `join-type` | `LEFT JOIN` and `INNER JOIN` swap |
|
|
139
|
+
| `arithmetic` | `+` and `-`, `*` and `/` swap; a unary minus is dropped |
|
|
140
|
+
| `literal` | an integer grows by one |
|
|
141
|
+
| `distinct` | `DISTINCT` is dropped from `SELECT` and `COUNT` |
|
|
142
|
+
| `case` | the `ELSE` value becomes `NULL`, or one `WHEN` branch is dropped |
|
|
143
|
+
| `string-literal` | a string gets a suffix |
|
|
144
|
+
| `column` | a column becomes the most similarly named other column of the same table |
|
|
145
|
+
|
|
146
|
+
A change that provably cannot alter any result is left out: `UNION ALL` and `UNION` return the same rows when
|
|
147
|
+
every branch returns unique rows and each pair of branches differs in a literal column.
|
|
148
|
+
|
|
149
|
+
## Configuration
|
|
150
|
+
|
|
151
|
+
Settings live in the tested project's `pyproject.toml`; command line options override them. Paths in the
|
|
152
|
+
settings are relative to the project, and patterns are globs, where `*` does not cross a directory.
|
|
153
|
+
|
|
154
|
+
```toml
|
|
155
|
+
[tool.pysqlmut]
|
|
156
|
+
dialect = "duckdb"
|
|
157
|
+
files = ["sql/**/*.sql"]
|
|
158
|
+
exclude-operators = ["union"]
|
|
159
|
+
accepted = "pysqlmut-accepted.json"
|
|
160
|
+
workers = 8 # processes that generate mutants, then parallel test runs
|
|
161
|
+
timeout = 180 # seconds per mutant
|
|
162
|
+
|
|
163
|
+
# Long-lived pytest workers that run only the tests reading the mutated file.
|
|
164
|
+
[tool.pysqlmut.pytest]
|
|
165
|
+
python = "uv run python"
|
|
166
|
+
tests = ["tests/sql"]
|
|
167
|
+
args = ["-q", "-x"]
|
|
168
|
+
|
|
169
|
+
[[tool.pysqlmut.file]]
|
|
170
|
+
pattern = "sql/labels.sql"
|
|
171
|
+
exclude-operators = ["string-literal"]
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Instead of `[tool.pysqlmut.pytest]`, `command = "..."` runs any test command, in a new process per mutant.
|
|
175
|
+
With the settings in place, `pysqlmut run` needs no arguments. Unknown settings and values of the wrong type
|
|
176
|
+
are reported before anything runs.
|
|
177
|
+
|
|
178
|
+
## Reviewing survivors
|
|
179
|
+
|
|
180
|
+
- `-- pysqlmut: skip` at the end of a line excludes that line; `-- pysqlmut: off` and `-- pysqlmut: on`
|
|
181
|
+
exclude a block, including changes that would reach into it.
|
|
182
|
+
- `pysqlmut accept pysqlmut-report.json` records the survivors of a report as reviewed, and later runs neither
|
|
183
|
+
run nor report them. A survivor is identified by its file, operator, description and the line before and
|
|
184
|
+
after the change, so edits elsewhere in the file keep it accepted. `--operators` accepts only some
|
|
185
|
+
operators. Commit the accepted file with the project.
|
|
186
|
+
- `pysqlmut generate` (also available as `pysqlmut list`) shows every mutant and why candidates were
|
|
187
|
+
rejected.
|
|
188
|
+
|
|
189
|
+
## Limitations
|
|
190
|
+
|
|
191
|
+
- With `--pytest`, a file read by a subprocess, a pytest-xdist worker or native code counts as not covered.
|
|
192
|
+
Use `--command` for such projects.
|
|
193
|
+
- Some survivors cannot change a result for any data your SQL can see, for example `MAX` to `MIN` on a value
|
|
194
|
+
that is the same in every row of its group. pysqlmut only removes such changes when it can prove it from the
|
|
195
|
+
SQL alone; accept the rest after review.
|
|
196
|
+
- `string-literal` and `case` produce many survivors on label and mapping SQL. Exclude them per file when their
|
|
197
|
+
survivors are not useful.
|
|
198
|
+
- Jinja templates, scripting blocks, procedures and other statements sqlglot cannot parse are not mutated.
|
|
199
|
+
- Windows is supported only through WSL.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
just setup # install dependencies and pre-commit hooks
|
|
205
|
+
just check # lint, type check, test
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`AGENTS.md` describes the design rules and what makes a test worth keeping. `RELEASING.md` describes how
|
|
209
|
+
versions are published, `SECURITY.md` how to report a vulnerability, and `CHANGELOG.md` lists the changes.
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pysqlmut"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Mutation testing for SQL: change SQL files in small, valid ways and report the changes your tests miss."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = [
|
|
10
|
+
"sql",
|
|
11
|
+
"mutation testing",
|
|
12
|
+
"testing",
|
|
13
|
+
"sqlglot",
|
|
14
|
+
"pytest",
|
|
15
|
+
"duckdb",
|
|
16
|
+
"data engineering",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 3 - Alpha",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: Developers",
|
|
22
|
+
"Operating System :: MacOS",
|
|
23
|
+
"Operating System :: POSIX :: Linux",
|
|
24
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: SQL",
|
|
29
|
+
"Topic :: Database",
|
|
30
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
31
|
+
"Topic :: Software Development :: Testing",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"sqlglot>=30.18,<31",
|
|
35
|
+
"typer>=0.27.2",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[[project.authors]]
|
|
39
|
+
name = "Michael Aydinbas"
|
|
40
|
+
email = "michael.aydinbas@pm.me"
|
|
41
|
+
|
|
42
|
+
[project.urls]
|
|
43
|
+
Homepage = "https://github.com/pmayd/pysqlmut"
|
|
44
|
+
Repository = "https://github.com/pmayd/pysqlmut"
|
|
45
|
+
Issues = "https://github.com/pmayd/pysqlmut/issues"
|
|
46
|
+
Changelog = "https://github.com/pmayd/pysqlmut/blob/main/CHANGELOG.md"
|
|
47
|
+
|
|
48
|
+
[project.scripts]
|
|
49
|
+
pysqlmut = "pysqlmut.cli:main"
|
|
50
|
+
|
|
51
|
+
[build-system]
|
|
52
|
+
requires = ["uv_build>=0.12.12,<0.13.0"]
|
|
53
|
+
build-backend = "uv_build"
|
|
54
|
+
|
|
55
|
+
[dependency-groups]
|
|
56
|
+
dev = [
|
|
57
|
+
"duckdb>=1.5.5",
|
|
58
|
+
"pre-commit>=4.6.2",
|
|
59
|
+
"pytest>=9.1.1",
|
|
60
|
+
"ruff>=0.16.7",
|
|
61
|
+
"ty>=0.0.80",
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 120
|
|
66
|
+
target-version = "py312"
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = [
|
|
70
|
+
"E",
|
|
71
|
+
"F",
|
|
72
|
+
"W",
|
|
73
|
+
"I",
|
|
74
|
+
"B",
|
|
75
|
+
"UP",
|
|
76
|
+
"SIM",
|
|
77
|
+
"RUF",
|
|
78
|
+
"C4",
|
|
79
|
+
"PT",
|
|
80
|
+
"PL",
|
|
81
|
+
"PTH",
|
|
82
|
+
"RET",
|
|
83
|
+
"S",
|
|
84
|
+
"BLE",
|
|
85
|
+
]
|
|
86
|
+
ignore = [
|
|
87
|
+
"PLR2004",
|
|
88
|
+
"PLR0911",
|
|
89
|
+
"PLR0913",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
[tool.ruff.lint.per-file-ignores]
|
|
93
|
+
"src/pysqlmut/cli.py" = ["PLR0917"]
|
|
94
|
+
"tests/*" = ["S101"]
|
|
95
|
+
"examples/*" = ["S101"]
|
|
96
|
+
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
testpaths = ["tests"]
|
|
99
|
+
addopts = [
|
|
100
|
+
"--import-mode=importlib",
|
|
101
|
+
"-ra",
|
|
102
|
+
]
|
|
103
|
+
pythonpath = ["tests"]
|
|
104
|
+
|
|
105
|
+
[tool.ty.environment]
|
|
106
|
+
extra-paths = ["tests"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pysqlmut"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Mutation testing for SQL: change SQL files in small, valid ways and report the changes your tests miss."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Michael Aydinbas", email = "michael.aydinbas@pm.me" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
license-files = ["LICENSE"]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
keywords = ["sql", "mutation testing", "testing", "sqlglot", "pytest", "duckdb", "data engineering"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: MacOS",
|
|
18
|
+
"Operating System :: POSIX :: Linux",
|
|
19
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Programming Language :: SQL",
|
|
24
|
+
"Topic :: Database",
|
|
25
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
# sqlglot changes its expression classes between major versions, which the operators rely on.
|
|
30
|
+
"sqlglot>=30.18,<31",
|
|
31
|
+
"typer>=0.27.2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/pmayd/pysqlmut"
|
|
36
|
+
Repository = "https://github.com/pmayd/pysqlmut"
|
|
37
|
+
Issues = "https://github.com/pmayd/pysqlmut/issues"
|
|
38
|
+
Changelog = "https://github.com/pmayd/pysqlmut/blob/main/CHANGELOG.md"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
pysqlmut = "pysqlmut.cli:main"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["uv_build>=0.12.12,<0.13.0"]
|
|
45
|
+
build-backend = "uv_build"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"duckdb>=1.5.5",
|
|
50
|
+
"pre-commit>=4.6.2",
|
|
51
|
+
"pytest>=9.1.1",
|
|
52
|
+
"ruff>=0.16.7",
|
|
53
|
+
"ty>=0.0.80",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 120
|
|
58
|
+
target-version = "py312"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "C4", "PT", "PL", "PTH", "RET", "S", "BLE"]
|
|
62
|
+
ignore = [
|
|
63
|
+
"PLR2004", # magic values read fine in tests and token handling
|
|
64
|
+
"PLR0911", # operators return early per node shape
|
|
65
|
+
"PLR0913", # run() takes its options as keyword arguments
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.per-file-ignores]
|
|
69
|
+
"src/pysqlmut/cli.py" = ["PLR0917"] # Typer turns each parameter of a command into an option
|
|
70
|
+
"tests/*" = ["S101"] # pytest asserts
|
|
71
|
+
"examples/*" = ["S101"] # pytest asserts in the example project
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
testpaths = ["tests"]
|
|
75
|
+
addopts = ["--import-mode=importlib", "-ra"]
|
|
76
|
+
pythonpath = ["tests"]
|
|
77
|
+
|
|
78
|
+
[tool.ty.environment]
|
|
79
|
+
extra-paths = ["tests"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Mutation testing for SQL."""
|