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 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.
@@ -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
+ [![CI](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml/badge.svg)](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml)
35
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
@@ -0,0 +1,213 @@
1
+ # pysqlmut
2
+
3
+ [![CI](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml/badge.svg)](https://github.com/pmayd/pysqlmut/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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."""