sprocguard 0.1.1__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.
- sprocguard-0.1.1/.gitignore +12 -0
- sprocguard-0.1.1/LICENSE +21 -0
- sprocguard-0.1.1/PKG-INFO +230 -0
- sprocguard-0.1.1/README.md +197 -0
- sprocguard-0.1.1/examples/cases/usp_GetCustomerOrders.json +5 -0
- sprocguard-0.1.1/examples/cases/usp_PlaceOrder.json +7 -0
- sprocguard-0.1.1/examples/demo.sql +95 -0
- sprocguard-0.1.1/examples/regression.sql +38 -0
- sprocguard-0.1.1/pyproject.toml +56 -0
- sprocguard-0.1.1/src/sprocguard/__init__.py +3 -0
- sprocguard-0.1.1/src/sprocguard/analysis.py +485 -0
- sprocguard-0.1.1/src/sprocguard/capture.py +331 -0
- sprocguard-0.1.1/src/sprocguard/cli.py +132 -0
- sprocguard-0.1.1/src/sprocguard/compare.py +386 -0
- sprocguard-0.1.1/src/sprocguard/config.py +144 -0
- sprocguard-0.1.1/src/sprocguard/db.py +148 -0
- sprocguard-0.1.1/src/sprocguard/server.py +107 -0
- sprocguard-0.1.1/src/sprocguard/service.py +77 -0
- sprocguard-0.1.1/src/sprocguard/tsqlt.py +180 -0
- sprocguard-0.1.1/tests/conftest.py +129 -0
- sprocguard-0.1.1/tests/fakedb.py +223 -0
- sprocguard-0.1.1/tests/test_analysis.py +70 -0
- sprocguard-0.1.1/tests/test_capture.py +118 -0
- sprocguard-0.1.1/tests/test_compare.py +53 -0
- sprocguard-0.1.1/tests/test_server_and_cli.py +53 -0
- sprocguard-0.1.1/tests/test_tsqlt.py +59 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
# Your local config usually holds a connection string; share a sample instead.
|
|
10
|
+
sprocguard.toml
|
|
11
|
+
# Baselines and tSQLt tests generated while trying the demo locally.
|
|
12
|
+
.sprocguard/
|
sprocguard-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jheyson Huarauya Riquelme
|
|
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,230 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sprocguard
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A safety net for SQL Server stored procedures: capture what a procedure does today, then verify an AI agent (or a human) didn't change it by accident.
|
|
5
|
+
Project-URL: Homepage, https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard
|
|
6
|
+
Project-URL: Repository, https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard
|
|
7
|
+
Project-URL: Issues, https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/releases
|
|
9
|
+
Author: Jheyson Huarauya Riquelme
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai-agents,characterization-tests,golden-master,mcp,sql-server,stored-procedures,t-sql,tsqlt
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: SQL
|
|
22
|
+
Classifier: Topic :: Database
|
|
23
|
+
Classifier: Topic :: Software Development :: Testing
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: mcp>=2.0
|
|
26
|
+
Requires-Dist: pyodbc>=5.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: sqlglot>=25; extra == 'dev'
|
|
30
|
+
Provides-Extra: pymssql
|
|
31
|
+
Requires-Dist: pymssql>=2.3; extra == 'pymssql'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# sprocguard
|
|
35
|
+
|
|
36
|
+
<!-- mcp-name: io.github.JheysonJhordyHuarauyaRiquelme/sprocguard -->
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/sprocguard/)
|
|
39
|
+
[](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/actions/workflows/ci.yml)
|
|
40
|
+
[](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/LICENSE)
|
|
41
|
+
[](https://pypi.org/project/sprocguard/)
|
|
42
|
+
|
|
43
|
+
**A safety net for SQL Server stored procedures.**
|
|
44
|
+
Capture what a procedure does *today*, let a human or an AI agent change it, and get a clear
|
|
45
|
+
report of every behavior that changed — before it reaches production.
|
|
46
|
+
|
|
47
|
+
[Leer en español](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/README.es.md)
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
$ sprocguard verify dbo.usp_PlaceOrder
|
|
51
|
+
FAIL dbo.usp_PlaceOrder: 3/5 case(s) behave exactly as in the baseline.
|
|
52
|
+
(the procedure definition changed since the baseline was captured)
|
|
53
|
+
ok regular customer buys 2 coffees
|
|
54
|
+
XX VIP customer gets 10% off
|
|
55
|
+
- Result set #1, row 1: Total "33.75" -> "33.80"
|
|
56
|
+
- dbo.Orders: no longer inserts {"CustomerId": 1, "Total": "33.75"}
|
|
57
|
+
- dbo.Orders: now inserts {"CustomerId": 1, "Total": "33.80"}
|
|
58
|
+
XX VIP discount with odd cents
|
|
59
|
+
- Result set #1, row 1: Total "78.75" -> "78.80"
|
|
60
|
+
- dbo.Orders: no longer inserts {"CustomerId": 1, "Total": "78.75"}
|
|
61
|
+
- dbo.Orders: now inserts {"CustomerId": 1, "Total": "78.80"}
|
|
62
|
+
ok buys exactly the last units
|
|
63
|
+
ok insufficient stock raises error
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Why
|
|
67
|
+
|
|
68
|
+
Many companies keep their business rules inside stored procedures, usually without tests.
|
|
69
|
+
AI coding agents are now asked to "clean up" or "optimize" them, and a refactor that looks
|
|
70
|
+
equivalent can quietly change a rounding rule, an error path or a table it updates.
|
|
71
|
+
|
|
72
|
+
sprocguard gives you **characterization tests** (also called *golden master* tests) in minutes:
|
|
73
|
+
it records what the procedure returns and what it changes in the database, then compares any
|
|
74
|
+
new version against that record.
|
|
75
|
+
|
|
76
|
+
## What it captures
|
|
77
|
+
|
|
78
|
+
For every case (a set of parameter values):
|
|
79
|
+
|
|
80
|
+
- every **result set**, in order when the order is stable;
|
|
81
|
+
- the **return value** and **OUTPUT parameters**;
|
|
82
|
+
- the **error** raised, if any (errors are behavior too);
|
|
83
|
+
- the **rows inserted, updated and deleted** in each table the procedure writes,
|
|
84
|
+
including tables written by procedures it calls and by triggers.
|
|
85
|
+
|
|
86
|
+
Each case runs **twice**. Values that change by themselves (new IDENTITY values, `NEWID()`, and any
|
|
87
|
+
date or time taken while the procedure was running, even at second or day precision) are detected
|
|
88
|
+
automatically and ignored when verifying.
|
|
89
|
+
|
|
90
|
+
## Safety model
|
|
91
|
+
|
|
92
|
+
sprocguard executes your procedures, so safety comes first:
|
|
93
|
+
|
|
94
|
+
| Protection | How |
|
|
95
|
+
|---|---|
|
|
96
|
+
| Nothing is ever committed | Each execution runs inside a transaction that is always rolled back, with `IMPLICIT_TRANSACTIONS ON` and two nesting levels, so an inner `ROLLBACK` or a stray `COMMIT` in the procedure cannot leak changes. |
|
|
97
|
+
| Only development databases | It refuses to connect to any database not listed in `allowed_databases`. |
|
|
98
|
+
| No irreversible side effects | Procedures that send e-mail, run `xp_cmdshell`, start jobs, use `OPENROWSET`/`OPENQUERY` or linked servers are refused by default: a rollback cannot undo them. |
|
|
99
|
+
| Big tables are not dumped | Tables above `max_rows_per_table` are not snapshotted (reported as skipped). |
|
|
100
|
+
| SQL injection | Parameter values are always sent as query parameters, never concatenated. |
|
|
101
|
+
|
|
102
|
+
Captures hold locks on the tables involved for a moment: use a development database.
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
Requires Python 3.11+ and the [Microsoft ODBC Driver 17 or 18 for SQL Server](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server)
|
|
107
|
+
(usually already installed with SQL Server or SSMS). Check which one you have with
|
|
108
|
+
`Get-OdbcDriver | Where-Object Name -like "*SQL Server*"` in PowerShell.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
pip install sprocguard
|
|
112
|
+
# SQL authentication without ODBC: pip install "sprocguard[pymssql]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Quick start (5 minutes, with the demo database)
|
|
116
|
+
|
|
117
|
+
The demo scripts live in the [`examples`](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/tree/main/examples) folder of the repository:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard.git
|
|
121
|
+
cd sprocguard
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
sqlcmd -S localhost -E -i examples/demo.sql # creates DemoShop with two procedures
|
|
126
|
+
sprocguard init # writes sprocguard.toml; edit the connection
|
|
127
|
+
sprocguard inspect dbo.usp_PlaceOrder # parameters, tables, risks
|
|
128
|
+
sprocguard capture dbo.usp_PlaceOrder --cases examples/cases/usp_PlaceOrder.json
|
|
129
|
+
sqlcmd -S localhost -E -i examples/regression.sql # a "harmless" refactor
|
|
130
|
+
sprocguard verify dbo.usp_PlaceOrder # catches the rounding change (exit code 2)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`sprocguard.toml` (use `ODBC Driver 17 for SQL Server` if that is the one you have):
|
|
134
|
+
|
|
135
|
+
```toml
|
|
136
|
+
[connection]
|
|
137
|
+
odbc = "Driver={ODBC Driver 18 for SQL Server};Server=localhost;Database=DemoShop;Trusted_Connection=yes;TrustServerCertificate=yes"
|
|
138
|
+
|
|
139
|
+
[safety]
|
|
140
|
+
allowed_databases = ["DemoShop"]
|
|
141
|
+
max_rows_per_table = 5000
|
|
142
|
+
allow_external_side_effects = false
|
|
143
|
+
|
|
144
|
+
[storage]
|
|
145
|
+
baseline_dir = ".sprocguard/baselines" # commit this folder
|
|
146
|
+
tsqlt_dir = ".sprocguard/tsqlt"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Use it with AI agents (MCP)
|
|
150
|
+
|
|
151
|
+
sprocguard ships an MCP server and an [Agent Skill](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/skills/sprocguard/SKILL.md) that teaches
|
|
152
|
+
agents the workflow: *inspect → design cases → capture → change → verify*.
|
|
153
|
+
|
|
154
|
+
**Claude Code** (plugin with the MCP server and the skill):
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
/plugin marketplace add JheysonJhordyHuarauyaRiquelme/sprocguard
|
|
158
|
+
/plugin install sprocguard@sprocguard
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Any MCP client** (VS Code, Cursor, Claude Desktop...):
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{ "mcpServers": { "sprocguard": { "command": "sprocguard", "args": ["serve"] } } }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
With [uv](https://docs.astral.sh/uv/) you can skip the install step: use `"command": "uvx", "args": ["sprocguard", "serve"]`.
|
|
168
|
+
sprocguard is also listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
169
|
+
`io.github.JheysonJhordyHuarauyaRiquelme/sprocguard`.
|
|
170
|
+
|
|
171
|
+
| Tool | What it does | Read-only |
|
|
172
|
+
|---|---|---|
|
|
173
|
+
| `inspect_procedure` | Parameters, tables read/written, procedures and triggers involved, risks | yes |
|
|
174
|
+
| `suggest_cases` | Real parameter values from related tables plus edge values | yes |
|
|
175
|
+
| `capture_baseline` | Records the current behavior (refuses to overwrite unless asked) | writes a file |
|
|
176
|
+
| `verify_procedure` | Re-runs the cases and lists every difference | yes |
|
|
177
|
+
| `generate_tsqlt_tests` | Turns a baseline into tSQLt tests | writes a file |
|
|
178
|
+
| `list_baselines` | Lists captured baselines | yes |
|
|
179
|
+
|
|
180
|
+
## CLI
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
sprocguard init create sprocguard.toml
|
|
184
|
+
sprocguard inspect <proc> [--definition] describe a procedure and its risks
|
|
185
|
+
sprocguard suggest <proc> suggest parameter values
|
|
186
|
+
sprocguard capture <proc> --cases cases.json capture a baseline [--watch schema.table ...] [--overwrite]
|
|
187
|
+
sprocguard verify [<proc> ...] verify one, several or all baselines (exit 2 if behavior changed)
|
|
188
|
+
sprocguard tsqlt <proc> generate tSQLt tests (read-only procedures)
|
|
189
|
+
sprocguard list list baselines
|
|
190
|
+
sprocguard serve run the MCP server (stdio)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Add `--json` for machine-readable output. In CI, run `sprocguard verify` against a test
|
|
194
|
+
database restored from a known backup: the job fails when any procedure changed behavior.
|
|
195
|
+
|
|
196
|
+
## tSQLt export
|
|
197
|
+
|
|
198
|
+
For read-only procedures, `sprocguard tsqlt <proc>` writes a `.sql` file with one
|
|
199
|
+
[tSQLt](https://tsqlt.org) test per case, using `tSQLt.AssertEqualsTable` and
|
|
200
|
+
`tSQLt.ExpectException`. Volatile columns are left out of the comparison.
|
|
201
|
+
|
|
202
|
+
## Limitations (0.1)
|
|
203
|
+
|
|
204
|
+
- Table-valued parameters are not supported yet.
|
|
205
|
+
- Tables changed only through dynamic SQL are not detected automatically; add them with `--watch`.
|
|
206
|
+
- If the procedure fails, result sets returned before the error are not recorded (the error is).
|
|
207
|
+
- tSQLt export covers read-only procedures; procedures that modify data use `verify`.
|
|
208
|
+
- Comparisons ignore the order of rows when the order was not stable between the two capture runs.
|
|
209
|
+
|
|
210
|
+
## Roadmap
|
|
211
|
+
|
|
212
|
+
- [ ] tSQLt export with `FakeTable` for procedures that modify data
|
|
213
|
+
- [ ] Table-valued parameters
|
|
214
|
+
- [ ] `sprocguard impact <table>`: verify every procedure that uses a table you are changing
|
|
215
|
+
- [ ] GitHub Action
|
|
216
|
+
- [x] Publish to PyPI and the MCP Registry
|
|
217
|
+
|
|
218
|
+
## Development
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install -e ".[dev]"
|
|
222
|
+
pytest
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The test suite uses an in-memory imitation of SQL Server (`tests/fakedb.py`), so it runs
|
|
226
|
+
anywhere; real-database testing uses `examples/demo.sql`.
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
MIT
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# sprocguard
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.JheysonJhordyHuarauyaRiquelme/sprocguard -->
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/sprocguard/)
|
|
6
|
+
[](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/LICENSE)
|
|
8
|
+
[](https://pypi.org/project/sprocguard/)
|
|
9
|
+
|
|
10
|
+
**A safety net for SQL Server stored procedures.**
|
|
11
|
+
Capture what a procedure does *today*, let a human or an AI agent change it, and get a clear
|
|
12
|
+
report of every behavior that changed — before it reaches production.
|
|
13
|
+
|
|
14
|
+
[Leer en español](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/README.es.md)
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
$ sprocguard verify dbo.usp_PlaceOrder
|
|
18
|
+
FAIL dbo.usp_PlaceOrder: 3/5 case(s) behave exactly as in the baseline.
|
|
19
|
+
(the procedure definition changed since the baseline was captured)
|
|
20
|
+
ok regular customer buys 2 coffees
|
|
21
|
+
XX VIP customer gets 10% off
|
|
22
|
+
- Result set #1, row 1: Total "33.75" -> "33.80"
|
|
23
|
+
- dbo.Orders: no longer inserts {"CustomerId": 1, "Total": "33.75"}
|
|
24
|
+
- dbo.Orders: now inserts {"CustomerId": 1, "Total": "33.80"}
|
|
25
|
+
XX VIP discount with odd cents
|
|
26
|
+
- Result set #1, row 1: Total "78.75" -> "78.80"
|
|
27
|
+
- dbo.Orders: no longer inserts {"CustomerId": 1, "Total": "78.75"}
|
|
28
|
+
- dbo.Orders: now inserts {"CustomerId": 1, "Total": "78.80"}
|
|
29
|
+
ok buys exactly the last units
|
|
30
|
+
ok insufficient stock raises error
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Why
|
|
34
|
+
|
|
35
|
+
Many companies keep their business rules inside stored procedures, usually without tests.
|
|
36
|
+
AI coding agents are now asked to "clean up" or "optimize" them, and a refactor that looks
|
|
37
|
+
equivalent can quietly change a rounding rule, an error path or a table it updates.
|
|
38
|
+
|
|
39
|
+
sprocguard gives you **characterization tests** (also called *golden master* tests) in minutes:
|
|
40
|
+
it records what the procedure returns and what it changes in the database, then compares any
|
|
41
|
+
new version against that record.
|
|
42
|
+
|
|
43
|
+
## What it captures
|
|
44
|
+
|
|
45
|
+
For every case (a set of parameter values):
|
|
46
|
+
|
|
47
|
+
- every **result set**, in order when the order is stable;
|
|
48
|
+
- the **return value** and **OUTPUT parameters**;
|
|
49
|
+
- the **error** raised, if any (errors are behavior too);
|
|
50
|
+
- the **rows inserted, updated and deleted** in each table the procedure writes,
|
|
51
|
+
including tables written by procedures it calls and by triggers.
|
|
52
|
+
|
|
53
|
+
Each case runs **twice**. Values that change by themselves (new IDENTITY values, `NEWID()`, and any
|
|
54
|
+
date or time taken while the procedure was running, even at second or day precision) are detected
|
|
55
|
+
automatically and ignored when verifying.
|
|
56
|
+
|
|
57
|
+
## Safety model
|
|
58
|
+
|
|
59
|
+
sprocguard executes your procedures, so safety comes first:
|
|
60
|
+
|
|
61
|
+
| Protection | How |
|
|
62
|
+
|---|---|
|
|
63
|
+
| Nothing is ever committed | Each execution runs inside a transaction that is always rolled back, with `IMPLICIT_TRANSACTIONS ON` and two nesting levels, so an inner `ROLLBACK` or a stray `COMMIT` in the procedure cannot leak changes. |
|
|
64
|
+
| Only development databases | It refuses to connect to any database not listed in `allowed_databases`. |
|
|
65
|
+
| No irreversible side effects | Procedures that send e-mail, run `xp_cmdshell`, start jobs, use `OPENROWSET`/`OPENQUERY` or linked servers are refused by default: a rollback cannot undo them. |
|
|
66
|
+
| Big tables are not dumped | Tables above `max_rows_per_table` are not snapshotted (reported as skipped). |
|
|
67
|
+
| SQL injection | Parameter values are always sent as query parameters, never concatenated. |
|
|
68
|
+
|
|
69
|
+
Captures hold locks on the tables involved for a moment: use a development database.
|
|
70
|
+
|
|
71
|
+
## Install
|
|
72
|
+
|
|
73
|
+
Requires Python 3.11+ and the [Microsoft ODBC Driver 17 or 18 for SQL Server](https://learn.microsoft.com/sql/connect/odbc/download-odbc-driver-for-sql-server)
|
|
74
|
+
(usually already installed with SQL Server or SSMS). Check which one you have with
|
|
75
|
+
`Get-OdbcDriver | Where-Object Name -like "*SQL Server*"` in PowerShell.
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install sprocguard
|
|
79
|
+
# SQL authentication without ODBC: pip install "sprocguard[pymssql]"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Quick start (5 minutes, with the demo database)
|
|
83
|
+
|
|
84
|
+
The demo scripts live in the [`examples`](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/tree/main/examples) folder of the repository:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
git clone https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard.git
|
|
88
|
+
cd sprocguard
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
sqlcmd -S localhost -E -i examples/demo.sql # creates DemoShop with two procedures
|
|
93
|
+
sprocguard init # writes sprocguard.toml; edit the connection
|
|
94
|
+
sprocguard inspect dbo.usp_PlaceOrder # parameters, tables, risks
|
|
95
|
+
sprocguard capture dbo.usp_PlaceOrder --cases examples/cases/usp_PlaceOrder.json
|
|
96
|
+
sqlcmd -S localhost -E -i examples/regression.sql # a "harmless" refactor
|
|
97
|
+
sprocguard verify dbo.usp_PlaceOrder # catches the rounding change (exit code 2)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`sprocguard.toml` (use `ODBC Driver 17 for SQL Server` if that is the one you have):
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[connection]
|
|
104
|
+
odbc = "Driver={ODBC Driver 18 for SQL Server};Server=localhost;Database=DemoShop;Trusted_Connection=yes;TrustServerCertificate=yes"
|
|
105
|
+
|
|
106
|
+
[safety]
|
|
107
|
+
allowed_databases = ["DemoShop"]
|
|
108
|
+
max_rows_per_table = 5000
|
|
109
|
+
allow_external_side_effects = false
|
|
110
|
+
|
|
111
|
+
[storage]
|
|
112
|
+
baseline_dir = ".sprocguard/baselines" # commit this folder
|
|
113
|
+
tsqlt_dir = ".sprocguard/tsqlt"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Use it with AI agents (MCP)
|
|
117
|
+
|
|
118
|
+
sprocguard ships an MCP server and an [Agent Skill](https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/blob/main/skills/sprocguard/SKILL.md) that teaches
|
|
119
|
+
agents the workflow: *inspect → design cases → capture → change → verify*.
|
|
120
|
+
|
|
121
|
+
**Claude Code** (plugin with the MCP server and the skill):
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
/plugin marketplace add JheysonJhordyHuarauyaRiquelme/sprocguard
|
|
125
|
+
/plugin install sprocguard@sprocguard
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Any MCP client** (VS Code, Cursor, Claude Desktop...):
|
|
129
|
+
|
|
130
|
+
```json
|
|
131
|
+
{ "mcpServers": { "sprocguard": { "command": "sprocguard", "args": ["serve"] } } }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
With [uv](https://docs.astral.sh/uv/) you can skip the install step: use `"command": "uvx", "args": ["sprocguard", "serve"]`.
|
|
135
|
+
sprocguard is also listed in the [MCP Registry](https://registry.modelcontextprotocol.io) as
|
|
136
|
+
`io.github.JheysonJhordyHuarauyaRiquelme/sprocguard`.
|
|
137
|
+
|
|
138
|
+
| Tool | What it does | Read-only |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `inspect_procedure` | Parameters, tables read/written, procedures and triggers involved, risks | yes |
|
|
141
|
+
| `suggest_cases` | Real parameter values from related tables plus edge values | yes |
|
|
142
|
+
| `capture_baseline` | Records the current behavior (refuses to overwrite unless asked) | writes a file |
|
|
143
|
+
| `verify_procedure` | Re-runs the cases and lists every difference | yes |
|
|
144
|
+
| `generate_tsqlt_tests` | Turns a baseline into tSQLt tests | writes a file |
|
|
145
|
+
| `list_baselines` | Lists captured baselines | yes |
|
|
146
|
+
|
|
147
|
+
## CLI
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
sprocguard init create sprocguard.toml
|
|
151
|
+
sprocguard inspect <proc> [--definition] describe a procedure and its risks
|
|
152
|
+
sprocguard suggest <proc> suggest parameter values
|
|
153
|
+
sprocguard capture <proc> --cases cases.json capture a baseline [--watch schema.table ...] [--overwrite]
|
|
154
|
+
sprocguard verify [<proc> ...] verify one, several or all baselines (exit 2 if behavior changed)
|
|
155
|
+
sprocguard tsqlt <proc> generate tSQLt tests (read-only procedures)
|
|
156
|
+
sprocguard list list baselines
|
|
157
|
+
sprocguard serve run the MCP server (stdio)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Add `--json` for machine-readable output. In CI, run `sprocguard verify` against a test
|
|
161
|
+
database restored from a known backup: the job fails when any procedure changed behavior.
|
|
162
|
+
|
|
163
|
+
## tSQLt export
|
|
164
|
+
|
|
165
|
+
For read-only procedures, `sprocguard tsqlt <proc>` writes a `.sql` file with one
|
|
166
|
+
[tSQLt](https://tsqlt.org) test per case, using `tSQLt.AssertEqualsTable` and
|
|
167
|
+
`tSQLt.ExpectException`. Volatile columns are left out of the comparison.
|
|
168
|
+
|
|
169
|
+
## Limitations (0.1)
|
|
170
|
+
|
|
171
|
+
- Table-valued parameters are not supported yet.
|
|
172
|
+
- Tables changed only through dynamic SQL are not detected automatically; add them with `--watch`.
|
|
173
|
+
- If the procedure fails, result sets returned before the error are not recorded (the error is).
|
|
174
|
+
- tSQLt export covers read-only procedures; procedures that modify data use `verify`.
|
|
175
|
+
- Comparisons ignore the order of rows when the order was not stable between the two capture runs.
|
|
176
|
+
|
|
177
|
+
## Roadmap
|
|
178
|
+
|
|
179
|
+
- [ ] tSQLt export with `FakeTable` for procedures that modify data
|
|
180
|
+
- [ ] Table-valued parameters
|
|
181
|
+
- [ ] `sprocguard impact <table>`: verify every procedure that uses a table you are changing
|
|
182
|
+
- [ ] GitHub Action
|
|
183
|
+
- [x] Publish to PyPI and the MCP Registry
|
|
184
|
+
|
|
185
|
+
## Development
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
pip install -e ".[dev]"
|
|
189
|
+
pytest
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The test suite uses an in-memory imitation of SQL Server (`tests/fakedb.py`), so it runs
|
|
193
|
+
anywhere; real-database testing uses `examples/demo.sql`.
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
[
|
|
2
|
+
{ "name": "regular customer buys 2 coffees", "params": { "@CustomerId": 2, "@ProductId": 10, "@Quantity": 2 } },
|
|
3
|
+
{ "name": "VIP customer gets 10% off", "params": { "@CustomerId": 1, "@ProductId": 10, "@Quantity": 3 } },
|
|
4
|
+
{ "name": "VIP discount with odd cents", "params": { "@CustomerId": 1, "@ProductId": 10, "@Quantity": 7 } },
|
|
5
|
+
{ "name": "buys exactly the last units", "params": { "@CustomerId": 2, "@ProductId": 11, "@Quantity": 2 } },
|
|
6
|
+
{ "name": "insufficient stock raises error", "params": { "@CustomerId": 2, "@ProductId": 11, "@Quantity": 5 } }
|
|
7
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
sprocguard demo database.
|
|
3
|
+
Run it in SSMS, Azure Data Studio or sqlcmd against a DEVELOPMENT SQL Server (2016 or later):
|
|
4
|
+
sqlcmd -S localhost -E -i examples\demo.sql
|
|
5
|
+
*/
|
|
6
|
+
IF DB_ID(N'DemoShop') IS NULL
|
|
7
|
+
CREATE DATABASE DemoShop;
|
|
8
|
+
GO
|
|
9
|
+
USE DemoShop;
|
|
10
|
+
GO
|
|
11
|
+
|
|
12
|
+
DROP PROCEDURE IF EXISTS dbo.usp_PlaceOrder;
|
|
13
|
+
DROP PROCEDURE IF EXISTS dbo.usp_GetCustomerOrders;
|
|
14
|
+
DROP TABLE IF EXISTS dbo.AuditLog;
|
|
15
|
+
DROP TABLE IF EXISTS dbo.Orders;
|
|
16
|
+
DROP TABLE IF EXISTS dbo.Products;
|
|
17
|
+
DROP TABLE IF EXISTS dbo.Customers;
|
|
18
|
+
GO
|
|
19
|
+
|
|
20
|
+
CREATE TABLE dbo.Customers (
|
|
21
|
+
CustomerId int NOT NULL PRIMARY KEY,
|
|
22
|
+
Name nvarchar(100) NOT NULL,
|
|
23
|
+
Segment varchar(20) NOT NULL
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE dbo.Products (
|
|
26
|
+
ProductId int NOT NULL PRIMARY KEY,
|
|
27
|
+
Name nvarchar(100) NOT NULL,
|
|
28
|
+
Price decimal(10,2) NOT NULL,
|
|
29
|
+
Stock int NOT NULL
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE dbo.Orders (
|
|
32
|
+
OrderId int IDENTITY(1,1) NOT NULL PRIMARY KEY,
|
|
33
|
+
CustomerId int NOT NULL REFERENCES dbo.Customers (CustomerId),
|
|
34
|
+
OrderDate datetime2(0) NOT NULL,
|
|
35
|
+
Total decimal(10,2) NOT NULL
|
|
36
|
+
);
|
|
37
|
+
CREATE TABLE dbo.AuditLog ( -- no primary key on purpose: sprocguard compares it as a multiset
|
|
38
|
+
Action varchar(50) NOT NULL,
|
|
39
|
+
At datetime2(3) NOT NULL
|
|
40
|
+
);
|
|
41
|
+
GO
|
|
42
|
+
|
|
43
|
+
INSERT dbo.Customers (CustomerId, Name, Segment) VALUES (1, N'Ana', 'VIP'), (2, N'Luis', 'Regular');
|
|
44
|
+
INSERT dbo.Products (ProductId, Name, Price, Stock) VALUES (10, N'Café', 12.50, 100), (11, N'Cacao', 8.00, 2);
|
|
45
|
+
SET IDENTITY_INSERT dbo.Orders ON;
|
|
46
|
+
INSERT dbo.Orders (OrderId, CustomerId, OrderDate, Total)
|
|
47
|
+
VALUES (1, 1, '2026-01-05T09:00:00', 25.00), (2, 1, '2026-02-07T15:30:00', 8.00);
|
|
48
|
+
SET IDENTITY_INSERT dbo.Orders OFF;
|
|
49
|
+
GO
|
|
50
|
+
|
|
51
|
+
CREATE OR ALTER PROCEDURE dbo.usp_GetCustomerOrders
|
|
52
|
+
@CustomerId int
|
|
53
|
+
AS
|
|
54
|
+
BEGIN
|
|
55
|
+
SET NOCOUNT ON;
|
|
56
|
+
IF NOT EXISTS (SELECT 1 FROM dbo.Customers WHERE CustomerId = @CustomerId)
|
|
57
|
+
THROW 50001, 'Customer not found', 1;
|
|
58
|
+
|
|
59
|
+
SELECT OrderId, OrderDate, Total
|
|
60
|
+
FROM dbo.Orders
|
|
61
|
+
WHERE CustomerId = @CustomerId
|
|
62
|
+
ORDER BY OrderId;
|
|
63
|
+
END;
|
|
64
|
+
GO
|
|
65
|
+
|
|
66
|
+
CREATE OR ALTER PROCEDURE dbo.usp_PlaceOrder
|
|
67
|
+
@CustomerId int,
|
|
68
|
+
@ProductId int,
|
|
69
|
+
@Quantity int,
|
|
70
|
+
@OrderId int OUTPUT
|
|
71
|
+
AS
|
|
72
|
+
BEGIN
|
|
73
|
+
SET NOCOUNT ON;
|
|
74
|
+
DECLARE @Price decimal(10,2), @Stock int, @Total decimal(10,2);
|
|
75
|
+
|
|
76
|
+
SELECT @Price = Price, @Stock = Stock FROM dbo.Products WHERE ProductId = @ProductId;
|
|
77
|
+
IF @Stock < @Quantity
|
|
78
|
+
THROW 50002, 'Insufficient stock', 1;
|
|
79
|
+
|
|
80
|
+
SET @Total = @Price * @Quantity;
|
|
81
|
+
IF EXISTS (SELECT 1 FROM dbo.Customers WHERE CustomerId = @CustomerId AND Segment = 'VIP')
|
|
82
|
+
SET @Total = ROUND(@Total * 0.90, 2); -- VIP customers get 10% off
|
|
83
|
+
|
|
84
|
+
INSERT INTO dbo.Orders (CustomerId, OrderDate, Total) VALUES (@CustomerId, SYSDATETIME(), @Total);
|
|
85
|
+
SET @OrderId = SCOPE_IDENTITY();
|
|
86
|
+
|
|
87
|
+
UPDATE dbo.Products SET Stock = Stock - @Quantity WHERE ProductId = @ProductId;
|
|
88
|
+
INSERT INTO dbo.AuditLog (Action, At) VALUES ('order', SYSDATETIME());
|
|
89
|
+
|
|
90
|
+
SELECT @OrderId AS OrderId, @Total AS Total;
|
|
91
|
+
RETURN 0;
|
|
92
|
+
END;
|
|
93
|
+
GO
|
|
94
|
+
|
|
95
|
+
PRINT 'DemoShop is ready.';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
A "harmless refactor" of dbo.usp_PlaceOrder, the kind an AI agent might write.
|
|
3
|
+
It looks cleaner, but it changes how the VIP discount is rounded.
|
|
4
|
+
Deploy it after capturing the baseline, then run: sprocguard verify dbo.usp_PlaceOrder
|
|
5
|
+
*/
|
|
6
|
+
USE DemoShop;
|
|
7
|
+
GO
|
|
8
|
+
|
|
9
|
+
CREATE OR ALTER PROCEDURE dbo.usp_PlaceOrder
|
|
10
|
+
@CustomerId int,
|
|
11
|
+
@ProductId int,
|
|
12
|
+
@Quantity int,
|
|
13
|
+
@OrderId int OUTPUT
|
|
14
|
+
AS
|
|
15
|
+
BEGIN
|
|
16
|
+
SET NOCOUNT ON;
|
|
17
|
+
DECLARE @Price decimal(10,2), @Stock int, @Total decimal(10,2);
|
|
18
|
+
|
|
19
|
+
SELECT @Price = Price, @Stock = Stock FROM dbo.Products WHERE ProductId = @ProductId;
|
|
20
|
+
IF @Stock < @Quantity
|
|
21
|
+
THROW 50002, 'Insufficient stock', 1;
|
|
22
|
+
|
|
23
|
+
-- "Simplified": one expression instead of two steps.
|
|
24
|
+
SET @Total = CAST(@Price * @Quantity
|
|
25
|
+
* CASE WHEN EXISTS (SELECT 1 FROM dbo.Customers
|
|
26
|
+
WHERE CustomerId = @CustomerId AND Segment = 'VIP')
|
|
27
|
+
THEN 0.9 ELSE 1 END AS decimal(10,1));
|
|
28
|
+
|
|
29
|
+
INSERT INTO dbo.Orders (CustomerId, OrderDate, Total) VALUES (@CustomerId, SYSDATETIME(), @Total);
|
|
30
|
+
SET @OrderId = SCOPE_IDENTITY();
|
|
31
|
+
|
|
32
|
+
UPDATE dbo.Products SET Stock = Stock - @Quantity WHERE ProductId = @ProductId;
|
|
33
|
+
INSERT INTO dbo.AuditLog (Action, At) VALUES ('order', SYSDATETIME());
|
|
34
|
+
|
|
35
|
+
SELECT @OrderId AS OrderId, @Total AS Total;
|
|
36
|
+
RETURN 0;
|
|
37
|
+
END;
|
|
38
|
+
GO
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.24"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sprocguard"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A safety net for SQL Server stored procedures: capture what a procedure does today, then verify an AI agent (or a human) didn't change it by accident."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
authors = [{ name = "Jheyson Huarauya Riquelme" }]
|
|
13
|
+
keywords = ["sql-server", "t-sql", "stored-procedures", "tsqlt", "mcp", "ai-agents", "characterization-tests", "golden-master"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: SQL",
|
|
24
|
+
"Topic :: Database",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"mcp>=2.0",
|
|
29
|
+
"pyodbc>=5.0",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
pymssql = ["pymssql>=2.3"]
|
|
34
|
+
dev = ["pytest>=8", "sqlglot>=25"]
|
|
35
|
+
|
|
36
|
+
[project.scripts]
|
|
37
|
+
sprocguard = "sprocguard.cli:main"
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard"
|
|
41
|
+
Repository = "https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard"
|
|
42
|
+
Issues = "https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/issues"
|
|
43
|
+
Changelog = "https://github.com/JheysonJhordyHuarauyaRiquelme/sprocguard/releases"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.version]
|
|
46
|
+
path = "src/sprocguard/__init__.py"
|
|
47
|
+
|
|
48
|
+
[tool.hatch.build.targets.wheel]
|
|
49
|
+
packages = ["src/sprocguard"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.sdist]
|
|
52
|
+
include = ["src", "tests", "examples", "README.md", "LICENSE", "pyproject.toml"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|