tsql-fabric-debugger 0.3.2__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.
- tsql_fabric_debugger-0.3.2/.gitignore +13 -0
- tsql_fabric_debugger-0.3.2/CHANGELOG.md +253 -0
- tsql_fabric_debugger-0.3.2/LICENSE +21 -0
- tsql_fabric_debugger-0.3.2/PKG-INFO +358 -0
- tsql_fabric_debugger-0.3.2/README.md +316 -0
- tsql_fabric_debugger-0.3.2/docs/DOCUMENTATION.md +664 -0
- tsql_fabric_debugger-0.3.2/pyproject.toml +86 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/__init__.py +36 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/cli.py +126 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/connection.py +147 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/dap.py +661 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/engine.py +2076 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/introspect.py +104 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/parser.py +691 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/py.typed +0 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/runner.py +242 -0
- tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/scanner.py +83 -0
- tsql_fabric_debugger-0.3.2/tests/conftest.py +212 -0
- tsql_fabric_debugger-0.3.2/tests/fixtures/demo_proc.sql +43 -0
- tsql_fabric_debugger-0.3.2/tests/test_cli.py +50 -0
- tsql_fabric_debugger-0.3.2/tests/test_dap_offline.py +521 -0
- tsql_fabric_debugger-0.3.2/tests/test_engine.py +323 -0
- tsql_fabric_debugger-0.3.2/tests/test_engine_offline.py +1310 -0
- tsql_fabric_debugger-0.3.2/tests/test_features_02.py +220 -0
- tsql_fabric_debugger-0.3.2/tests/test_integration.py +725 -0
- tsql_fabric_debugger-0.3.2/tests/test_mutation_hardening.py +341 -0
- tsql_fabric_debugger-0.3.2/tests/test_parser.py +235 -0
- tsql_fabric_debugger-0.3.2/tests/test_runner.py +38 -0
- tsql_fabric_debugger-0.3.2/tests/test_scanner.py +36 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.2 — 2026-09-07
|
|
4
|
+
|
|
5
|
+
- **Result sets grouped per step**: the `tsqlFabricResultSet` DAP event now
|
|
6
|
+
carries `{line, sets: [{columns, rows, truncated}]}` — one event per step,
|
|
7
|
+
with **all** of that step's result sets under `sets`. A statement that
|
|
8
|
+
returns several result sets is delivered together instead of only the last
|
|
9
|
+
one. (Event shape change; consumers should read `sets`.)
|
|
10
|
+
|
|
11
|
+
## 0.3.1 — 2026-09-07
|
|
12
|
+
|
|
13
|
+
Hardening round (concurrency + privacy review):
|
|
14
|
+
|
|
15
|
+
- **`save_state()` privacy**: the docstring and the on-save message now warn
|
|
16
|
+
that the JSON file is plain-text and may contain warehouse/production data —
|
|
17
|
+
store it safely and delete when done.
|
|
18
|
+
- **Teardown robustness**: `dap.py` `_close_root` detaches the debugger before
|
|
19
|
+
closing, so a `SIGTERM`/`SIGINT` arriving mid-`close()` cannot leave a
|
|
20
|
+
half-torn-down session (idempotent even under `BaseException`).
|
|
21
|
+
- **Docs hygiene**: example procedure/parameter names generalized
|
|
22
|
+
(`dbo.load_sales` / `@year`) across README, docstrings and the CHANGELOG —
|
|
23
|
+
no internal/proprietary identifiers in the published package.
|
|
24
|
+
|
|
25
|
+
## 0.3.0 — 2026-09-04
|
|
26
|
+
|
|
27
|
+
Debugger parity with mainstream tools (pdb/debugpy, Chrome DevTools), driven
|
|
28
|
+
by a feature-gap analysis:
|
|
29
|
+
|
|
30
|
+
- **`step_out()`**: finish the current context and stop one level up — the
|
|
31
|
+
remaining sub-steps of an expanded IF/WHILE (a WHILE stops at its
|
|
32
|
+
re-evaluation step), or the whole active child debugger (OUTPUTs
|
|
33
|
+
collected). Honors breakpoints on the way.
|
|
34
|
+
- **`eval(expr)`**: one-shot server-side evaluation of a T-SQL expression
|
|
35
|
+
with the current variables — the single-use counterpart of `watch()`.
|
|
36
|
+
- **`stack()`**: the frame stack — procedures in the nested-EXEC chain plus
|
|
37
|
+
the expanded-block frames (loop iteration included).
|
|
38
|
+
- **`stop_on_error="any"`**: `run_all()`/`run_until()`/`step_out()` also
|
|
39
|
+
pause on errors a CATCH handled (after the CATCH emulation) — the
|
|
40
|
+
"break on caught exceptions" of DevTools.
|
|
41
|
+
- **`break_at(..., hits=N, once=True)`**: hit-count breakpoints (fire from
|
|
42
|
+
the Nth pass) and one-shot breakpoints. `breaks()` now returns
|
|
43
|
+
`{line: {"condition", "hits", "once", "count"}}` (was `{line: condition}`).
|
|
44
|
+
- **Logpoints**: `log_at(line, expr)` echoes a value when a line executes,
|
|
45
|
+
without ever stopping (`clear_logpoints()`, `logpoints()`). Fires inside
|
|
46
|
+
auto-expanded blocks and on IF/WHILE headers.
|
|
47
|
+
- **DAP adapter**: `tsql-fabric-dap` speaks the Debug Adapter Protocol over
|
|
48
|
+
stdio — debug the `.sql` visually from VS Code (via a DAP bridge
|
|
49
|
+
extension), nvim-dap or any DAP client: gutter/conditional/hit-count
|
|
50
|
+
breakpoints, step over/into/out, variables pane, hover/REPL evaluation,
|
|
51
|
+
CATCH-handled-error exception filter. Never commits; disconnect rolls back.
|
|
52
|
+
Result sets a step returns are printed to the Debug Console as a text table
|
|
53
|
+
and emitted as a `tsqlFabricResultSet` custom event
|
|
54
|
+
(`{line, columns, rows, truncated}`) for clients that render a grid.
|
|
55
|
+
|
|
56
|
+
Hardening from a 4-persona review (data engineer, data analyst, QA, DBA):
|
|
57
|
+
expression validation that blocks batch-breaking typos (comments, `;`,
|
|
58
|
+
unbalanced quotes) in watch/eval/logpoint/break conditions; broken breakpoint
|
|
59
|
+
conditions pause instead of crashing; `reset()` zeroes breakpoint hit
|
|
60
|
+
counters; a WHILE hitting `max_loop_iterations` pauses `run_all()` instead of
|
|
61
|
+
silently running post-loop steps on partial state; `eval()` failures no
|
|
62
|
+
longer pollute `ERROR_MESSAGE()`; logpoints are disarmed during CATCH
|
|
63
|
+
emulation.
|
|
64
|
+
|
|
65
|
+
Robustness against orphaned warehouse sessions (a debugger process killed
|
|
66
|
+
without close() leaves its transaction open, holding locks):
|
|
67
|
+
|
|
68
|
+
- **`kill_orphan_sessions(server, database, min_idle_seconds=900)`** (and
|
|
69
|
+
`tsql-debug --kill-orphans [--min-idle N]`): KILL library-tagged sessions
|
|
70
|
+
sleeping with an open transaction past the idle threshold, so the server
|
|
71
|
+
rolls them back and releases their locks.
|
|
72
|
+
- The `tsql-debug` and `tsql-fabric-dap` entry points install a SIGTERM
|
|
73
|
+
handler: a polite kill runs close()+ROLLBACK instead of orphaning the
|
|
74
|
+
session (SIGKILL still needs the janitor above).
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
## 0.2.3 — 2026-09-04
|
|
78
|
+
|
|
79
|
+
Usability (driven by end-user feedback):
|
|
80
|
+
|
|
81
|
+
- **`proc_name=`**: debug a DEPLOYED procedure by name — no more manual
|
|
82
|
+
`OBJECT_DEFINITION` boilerplate. `TSQLDebugger(proc_name="dbo.load_sales", ...)`
|
|
83
|
+
and `run_procedure(proc_name=..., ...)` fetch the source straight from the
|
|
84
|
+
warehouse on a short-lived session (a name without schema resolves to dbo).
|
|
85
|
+
Exactly one of `sql_file`/`sql_text`/`proc_name` must be given.
|
|
86
|
+
- **`fetch_source(proc_name, server, database)`** is now public, for when you
|
|
87
|
+
want the source text itself.
|
|
88
|
+
|
|
89
|
+
## 0.2.2 — 2026-09-04
|
|
90
|
+
|
|
91
|
+
Usability (driven by end-user feedback):
|
|
92
|
+
|
|
93
|
+
- **`run_all(into=True)`**: run the whole procedure the `step_into()` way —
|
|
94
|
+
every `IF`/`WHILE` is expanded, so each branch taken and each loop
|
|
95
|
+
iteration becomes its own logged step. Replaces the low-level
|
|
96
|
+
`while not dbg._finished and dbg._pos < len(dbg._steps): dbg.step_into()`
|
|
97
|
+
loop with a single call. Statements that are not blocks (and loops with
|
|
98
|
+
`BREAK`/`CONTINUE`) run whole, exactly as `run_all()` already does. Fully
|
|
99
|
+
backward compatible — the default is `into=False`.
|
|
100
|
+
- **`show_error()` / `last_error()`**: inspect a failure without scanning the
|
|
101
|
+
log by hand. `last_error()` returns the most recent ERROR log entry (or
|
|
102
|
+
`None`); `show_error()` runs `show_detail()` on it — replacing the
|
|
103
|
+
`erro = next(e for e in dbg._log if e["status"] == "ERROR");
|
|
104
|
+
dbg.show_detail(erro["step"])` idiom with a single call.
|
|
105
|
+
|
|
106
|
+
## 0.2.1 — 2026-09-04
|
|
107
|
+
|
|
108
|
+
Usability (driven by end-user feedback that the API was too low-level):
|
|
109
|
+
|
|
110
|
+
- **`summarize(log)`**: a one-glance verdict of a run — "OK, all N steps ran"
|
|
111
|
+
or "FAILED at step X (line Y): <message>", noting when a CATCH handled it.
|
|
112
|
+
Returns the facts as a dict (`ok`, `steps`, `error_step`, `error_line`,
|
|
113
|
+
`error`, `handled`). Pairs with `run_procedure` for a simple "run and tell
|
|
114
|
+
me what happened" flow, no step-by-step needed.
|
|
115
|
+
- **`find_step(contains=... | line=...)`**: locate a step by a text fragment
|
|
116
|
+
or a file line, instead of hand-writing
|
|
117
|
+
`next(i for i, s in enumerate(dbg._steps, 1) if ...)`.
|
|
118
|
+
- **`jump_to` and `run_until` now accept a text fragment or `line=<n>`**, not
|
|
119
|
+
only a step number — `dbg.run_until("MAX(SEQREC)")`, `dbg.jump_to(line=40)`.
|
|
120
|
+
Fully backward compatible (a number still works exactly as before).
|
|
121
|
+
|
|
122
|
+
## 0.2.0 — 2026-09-03
|
|
123
|
+
|
|
124
|
+
Productivity release — the items that turn a step executor into a debugger:
|
|
125
|
+
|
|
126
|
+
- **Nested EXEC step-into**: `step_into()` on `EXEC schema.proc ...` fetches
|
|
127
|
+
the child's source from the warehouse and returns a child debugger that
|
|
128
|
+
shares the parent's session/transaction; OUTPUT arguments and @@ROWCOUNT
|
|
129
|
+
copy back on completion, and an unhandled child error propagates to the
|
|
130
|
+
parent's CATCH exactly like the real EXEC. `abort_child()` discards.
|
|
131
|
+
Dynamic SQL/sp_executesql/expression arguments fall back to step-over.
|
|
132
|
+
|
|
133
|
+
- **Breakpoints**: `break_at(line, condition=None)` stops `run_all()` BEFORE
|
|
134
|
+
the matching step; file lines are stable across expansions, conditions run
|
|
135
|
+
server-side with the current variables, and `run_all()` auto-expands
|
|
136
|
+
IF/WHILE blocks that contain a breakpoint. `clear_breaks()`, `breaks()`.
|
|
137
|
+
- **Watches**: `watch(expr, name)` appends expressions to every capture
|
|
138
|
+
batch — values echo after each step and via `watches()`. `unwatch()`.
|
|
139
|
+
- **State snapshots**: `save_state(path)` / `load_state(source)` serialize
|
|
140
|
+
the variable environment (datetime/Decimal/bytes-safe JSON) — pair with
|
|
141
|
+
`jump_to()` to resume a session another day.
|
|
142
|
+
- **Replay**: `reset()` rolls back, restores the pristine step plan and the
|
|
143
|
+
initial parameter values, and replays from step 1 on a fresh connection.
|
|
144
|
+
- **`;`-less T-SQL**: statements now also split on the next statement-starting
|
|
145
|
+
keyword at level 0, with legal mid-statement continuations respected
|
|
146
|
+
(INSERT..SELECT, UPDATE..SET, WITH..consumer; MERGE never auto-splits).
|
|
147
|
+
Legacy code without terminators debugs statement by statement.
|
|
148
|
+
- **Execution diffs**: `diff_logs(log_a, log_b)` aligns two runs by
|
|
149
|
+
(line, kind) and reports only the divergences.
|
|
150
|
+
- **Large-value offload** (`offload_threshold`): strings above the threshold
|
|
151
|
+
live in a server-side session temp table and are hydrated into variables
|
|
152
|
+
per batch — uploaded once per change instead of re-sent on every step
|
|
153
|
+
(graceful fallback if the endpoint lacks temp tables).
|
|
154
|
+
- **`lock_timeout`**: a session opened with `lock_timeout=<seconds>` fails a
|
|
155
|
+
lock-blocked statement fast (error 1222) instead of hanging behind another
|
|
156
|
+
session's lock — the anti-hang for orphaned-transaction locks (constructor,
|
|
157
|
+
`run_script`, and `--lock-timeout` on the CLI). It bounds the wait; only
|
|
158
|
+
the server can reap the orphan itself.
|
|
159
|
+
- **Memory bounds**: `history_batches=N` prunes old SUCCESS payloads
|
|
160
|
+
(batch text/result sets) keeping the last N and every ERROR; `step_into`
|
|
161
|
+
on WHILE now prunes the previous iteration's executed sub-steps, so long
|
|
162
|
+
loops no longer grow the step list per iteration.
|
|
163
|
+
- **Test coverage**: a programmable fake pyodbc session (`tests/conftest.py`)
|
|
164
|
+
drives the engine offline, adding 32 engine tests and bringing `engine.py`
|
|
165
|
+
into the mutation-testing scope (previously scanner/parser only).
|
|
166
|
+
|
|
167
|
+
Post-implementation adversarial review (second pass) fixed: UNION/EXCEPT/
|
|
168
|
+
INTERSECT no longer split a statement (with or without ';'); a child ending
|
|
169
|
+
in error propagates to the parent CATCH instead of reporting SUCCESS;
|
|
170
|
+
breakpoints on a block's own header line stop before the block (and blocks
|
|
171
|
+
containing breakpoints auto-expand only for BODY lines); WHILE loops with
|
|
172
|
+
BREAK/CONTINUE containing a breakpoint stop before the loop instead of
|
|
173
|
+
silently running through; detached children cannot silently reconnect as
|
|
174
|
+
independent sessions; child guards on step_into/run_step/run_until; table
|
|
175
|
+
variables are rejected as EXEC arguments; the server-side offload table is
|
|
176
|
+
namespaced per debugger instance (parent/child same-named variables never
|
|
177
|
+
collide) and its creation state travels between parent and child; nested
|
|
178
|
+
loop pruning handles inner loops; reset()/close() detach an active child.
|
|
179
|
+
|
|
180
|
+
## 0.1.0 — 2026-09-03
|
|
181
|
+
|
|
182
|
+
First release.
|
|
183
|
+
|
|
184
|
+
Fixes from the four-lens pre-publish review (internal review —
|
|
185
|
+
data analyst, data engineer, DBA and developer/QA perspectives):
|
|
186
|
+
|
|
187
|
+
- **CLI**: exit-code computation no longer crashes on the base install
|
|
188
|
+
(without pandas); quoted `--param` values force strings (leading zeros,
|
|
189
|
+
literal "NULL"); strict int/float inference (no `1e5`/`nan`/`inf` floats);
|
|
190
|
+
missing file returns exit 2 with a clean message; loose-script fallback
|
|
191
|
+
warns and honors `--commit`; `--step-timeout` exposed; documented exit
|
|
192
|
+
codes.
|
|
193
|
+
- **Engine correctness**: post-CATCH skip covers nested TRY blocks
|
|
194
|
+
(catch-id stack); connection/internal failures are echoed as `[FATAL]` and
|
|
195
|
+
re-raised instead of silently finishing; the error entry (not the CATCH's
|
|
196
|
+
last entry) is returned to the caller; `DECLARE` and `RETURN` inside the
|
|
197
|
+
emulated CATCH work; bare `THROW` in the CATCH aborts like the real
|
|
198
|
+
re-raise; step_into condition failures route through the CATCH like
|
|
199
|
+
step(); the full `ERROR_*()` family is emulated; session `SET` options
|
|
200
|
+
persist (unparameterized batches); result sets produced before a failure
|
|
201
|
+
are kept.
|
|
202
|
+
- **Parser**: `IF ... ELSE` without `;` before the ELSE; `COPY`/`GRANT`/
|
|
203
|
+
`DENY`/`REVOKE`/`DBCC` end an IF condition; bodies without an outer
|
|
204
|
+
`BEGIN...END` (incl. starting at `BEGIN TRY`) parse correctly; catch
|
|
205
|
+
registration is idempotent across WHILE re-expansions; multi-encoding
|
|
206
|
+
`.sql` reading (UTF-8/BOM, UTF-16 BOM, cp1252).
|
|
207
|
+
- **Session lifecycle (DBA)**: context-manager support (`with ... as dbg:`);
|
|
208
|
+
exception-safe `close()`; public `rollback()`; Ctrl+C cancels the running
|
|
209
|
+
statement server-side and rolls back; ad-hoc `sql()` errors roll back and
|
|
210
|
+
are capped at 10k rows; transaction-control detection now covers string
|
|
211
|
+
literals (dynamic SQL) and flags opaque `EXEC` calls; `autocommit=True`
|
|
212
|
+
echoes a persistence warning; `APP=tsql-fabric-debugger` + `LoginTimeout`
|
|
213
|
+
on the connection string.
|
|
214
|
+
- **Observability**: `rows_affected` is `None` when not measured (no more
|
|
215
|
+
stale values); `post_rollback` column marks steps after a rollback;
|
|
216
|
+
`show_detail()` prints untruncated changed variables and the raw driver
|
|
217
|
+
error; `last_results()` DataFrames carry `attrs["truncated"]`;
|
|
218
|
+
multi-message SQL errors are joined instead of truncated; loose scripts
|
|
219
|
+
run inside a transaction with ROLLBACK by default and split via the
|
|
220
|
+
library's scanner; CSV saving works without pandas and uses utf-8-sig.
|
|
221
|
+
|
|
222
|
+
- `TSQLDebugger`: interactive procedure debugging without touching the
|
|
223
|
+
`.sql` — `step()`, `step_into()` (IF/WHILE statement by statement, with
|
|
224
|
+
the condition evaluated server-side), `run_until()`, `jump_to()`,
|
|
225
|
+
`run_step()`, `set_var()`, `show_vars()`, `sql()`, `show_detail()`,
|
|
226
|
+
`last_results()`, `set_log_level()`.
|
|
227
|
+
- State preserved between steps (DECLARE + re-injection through pyodbc
|
|
228
|
+
parameters + capture); `@@ROWCOUNT` and `ERROR_MESSAGE()` keep their
|
|
229
|
+
cross-batch semantics.
|
|
230
|
+
- `BEGIN CATCH` emulated; DECLARE inside blocks stays visible (batch scope).
|
|
231
|
+
- Transaction with ROLLBACK by default; `close(commit=True)` is explicit.
|
|
232
|
+
- Entra ID authentication: Fabric notebook (notebookutils) or `az login`
|
|
233
|
+
(AzureCliCredential).
|
|
234
|
+
- Batch mode (`run_procedure`), loose scripts (`run_script`) and a CLI
|
|
235
|
+
(`tsql-debug`).
|
|
236
|
+
- Mutation testing with mutmut plus a snapshot/invariant hardening suite.
|
|
237
|
+
|
|
238
|
+
Semantics fixes from the pre-release gap analysis
|
|
239
|
+
(internal gap analysis):
|
|
240
|
+
|
|
241
|
+
- `RETURN` ends the debug — including when it runs inside an atomic block
|
|
242
|
+
(detected by the missing capture).
|
|
243
|
+
- One CATCH block per `BEGIN TRY` (`catch_id` per step); after emulating the
|
|
244
|
+
CATCH, the debug skips the rest of that TRY and continues after
|
|
245
|
+
`END CATCH`, like T-SQL does.
|
|
246
|
+
- Table variables: declared in every batch, excluded from re-injection and
|
|
247
|
+
capture, with a warning that their content does not survive across steps.
|
|
248
|
+
- `BEGIN TRAN` is no longer treated as a `BEGIN...END` block opener
|
|
249
|
+
(procedures with explicit transactions used to break the parse) and inner
|
|
250
|
+
`COMMIT`/`ROLLBACK`/`BEGIN TRAN`/`SAVE TRAN` raise a warning.
|
|
251
|
+
- Result sets produced by the procedure itself are captured
|
|
252
|
+
(`last_results()`, `result_sets` column in the log) instead of discarded.
|
|
253
|
+
- `step_timeout` (seconds per step) in the constructor.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RedRex
|
|
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,358 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tsql-fabric-debugger
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Step-by-step debugger for T-SQL stored procedures on the Microsoft Fabric Warehouse
|
|
5
|
+
Project-URL: Homepage, https://github.com/redrex-tech/tsql_fabric_debugger
|
|
6
|
+
Project-URL: Repository, https://github.com/redrex-tech/tsql_fabric_debugger
|
|
7
|
+
Project-URL: Documentation, https://github.com/redrex-tech/tsql_fabric_debugger/blob/main/docs/DOCUMENTATION.md
|
|
8
|
+
Project-URL: Changelog, https://github.com/redrex-tech/tsql_fabric_debugger/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/redrex-tech/tsql_fabric_debugger/issues
|
|
10
|
+
Author: RedRex
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: debugger,fabric,microsoft-fabric,sql-server,tsql,warehouse
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: pyodbc>=5.0
|
|
28
|
+
Provides-Extra: all
|
|
29
|
+
Requires-Dist: azure-identity>=1.15; extra == 'all'
|
|
30
|
+
Requires-Dist: pandas>=2.0; extra == 'all'
|
|
31
|
+
Requires-Dist: sqlparse>=0.5; extra == 'all'
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: azure-identity>=1.15; extra == 'dev'
|
|
34
|
+
Requires-Dist: mutmut>=3.2; extra == 'dev'
|
|
35
|
+
Requires-Dist: pandas>=2.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
|
+
Provides-Extra: local
|
|
38
|
+
Requires-Dist: azure-identity>=1.15; extra == 'local'
|
|
39
|
+
Provides-Extra: pandas
|
|
40
|
+
Requires-Dist: pandas>=2.0; extra == 'pandas'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# tsql-fabric-debugger
|
|
44
|
+
|
|
45
|
+
Step-by-step debugger for T-SQL stored procedures on the **Microsoft Fabric
|
|
46
|
+
Warehouse** — without changing a single line of your `.sql`.
|
|
47
|
+
|
|
48
|
+
> This README is the quick tour. The deep dive — why Fabric has no native
|
|
49
|
+
> debugger, how the state engine works inside, the full API reference, and
|
|
50
|
+
> usage from a local machine, a Fabric notebook or AI agents/MCP — lives in
|
|
51
|
+
> [`docs/DOCUMENTATION.md`](docs/DOCUMENTATION.md).
|
|
52
|
+
|
|
53
|
+
## Why
|
|
54
|
+
|
|
55
|
+
The Fabric Warehouse has no T-SQL debugger: no breakpoints, no watch, no way
|
|
56
|
+
to run half a procedure. A whole procedure is a single statement to any SQL
|
|
57
|
+
client, and `DECLARE` variables die at the end of each batch — so "run one
|
|
58
|
+
piece at a time" doesn't work naively.
|
|
59
|
+
|
|
60
|
+
This library solves that by parsing the procedure **in memory**: it slices
|
|
61
|
+
the body into steps and preserves variable state between them. Each step runs
|
|
62
|
+
as one batch on the same session:
|
|
63
|
+
|
|
64
|
+
```sql
|
|
65
|
+
DECLARE <all variables>;
|
|
66
|
+
SELECT @a = ?, @b = ?, ...; -- state re-injection (pyodbc parameters)
|
|
67
|
+
<original statement, untouched>;
|
|
68
|
+
SELECT '__hcap__', @@ROWCOUNT, @a, @b, ...; -- capture of the new state
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`@@ROWCOUNT` and `ERROR_MESSAGE()` are rewritten to preserve cross-batch
|
|
72
|
+
semantics, and the procedure's `BEGIN CATCH` is emulated when a step fails.
|
|
73
|
+
Everything runs inside a transaction with **ROLLBACK at the end by
|
|
74
|
+
default** — nothing persists in the Warehouse unless you ask for it.
|
|
75
|
+
|
|
76
|
+
## Installation
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install tsql-fabric-debugger # Fabric notebook
|
|
80
|
+
pip install "tsql-fabric-debugger[local]" # local machine (az login)
|
|
81
|
+
pip install "tsql-fabric-debugger[all]" # + pandas and sqlparse
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Outside Fabric you need the **ODBC Driver 18 for SQL Server** installed and
|
|
85
|
+
a valid `az login`.
|
|
86
|
+
|
|
87
|
+
## IDE debugging (DAP)
|
|
88
|
+
|
|
89
|
+
`tsql-fabric-dap` speaks the Debug Adapter Protocol over stdio: point any DAP
|
|
90
|
+
client (VS Code, nvim-dap, ...) at it and debug the `.sql` visually — gutter
|
|
91
|
+
breakpoints, step over/into/out, variables pane, hover evaluation. Never
|
|
92
|
+
commits; disconnect rolls back.
|
|
93
|
+
|
|
94
|
+
Rows a step returns are surfaced two ways: printed as a text table to the
|
|
95
|
+
Debug Console, and emitted as a `tsqlFabricResultSet` custom DAP event
|
|
96
|
+
(`{line, sets: [{columns, rows, truncated}]}` — one event per step, carrying
|
|
97
|
+
all of that step's result sets) so a client can render them in a grid — the
|
|
98
|
+
VS Code extension shows a **Result Set** panel beside the editor.
|
|
99
|
+
|
|
100
|
+
A **VS Code extension** that wires this up (F5 on a `.sql`, no launch.json
|
|
101
|
+
needed) lives in [`editors/vscode/`](editors/vscode/) — preview/MVP.
|
|
102
|
+
|
|
103
|
+
## Interactive usage
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from tsql_fabric_debugger import TSQLDebugger
|
|
107
|
+
|
|
108
|
+
dbg = TSQLDebugger(
|
|
109
|
+
"prd_load.sql", # the original .sql, untouched
|
|
110
|
+
params={"@year": 2015}, # test values
|
|
111
|
+
server="<endpoint>.datawarehouse.fabric.microsoft.com",
|
|
112
|
+
database="my_warehouse",
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
No local `.sql`? Debug the **deployed** procedure by name — the source is
|
|
117
|
+
fetched straight from the warehouse (`OBJECT_DEFINITION`):
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
dbg = TSQLDebugger(proc_name="dbo.load_sales",
|
|
121
|
+
params={"@year": 2015}, server=..., database=...)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Prefer the context-manager form — it guarantees ROLLBACK + close even when an
|
|
125
|
+
exception interrupts the session, so no orphan transaction is left holding
|
|
126
|
+
locks on the warehouse:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
with TSQLDebugger("prd_load.sql", params={"@year": 2015},
|
|
130
|
+
server=..., database=...) as dbg:
|
|
131
|
+
dbg.run_all()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
|
|
136
|
+
dbg.list_steps() # list the numbered steps, without executing
|
|
137
|
+
dbg.step() # run the next step ("step over": whole IF/WHILE)
|
|
138
|
+
dbg.step_into() # step into an IF/WHILE: evaluates the condition
|
|
139
|
+
# server-side, picks the branch, yields sub-steps
|
|
140
|
+
dbg.step_out() # finish the current block/child and stop one level up
|
|
141
|
+
dbg.run_all(into=True) # run to the end the step_into() way: every IF/WHILE
|
|
142
|
+
# expanded, each loop iteration its own logged step
|
|
143
|
+
child = dbg.step_into() # on an `EXEC dbo.child ...` step: fetches the child's
|
|
144
|
+
# source from the warehouse and returns a CHILD
|
|
145
|
+
# debugger sharing this session; debug it, then the
|
|
146
|
+
# parent's next step() collects the OUTPUT values
|
|
147
|
+
# (an unhandled child error reaches the parent CATCH)
|
|
148
|
+
dbg.run_until(15) # run up to step 15 (breakpoint)
|
|
149
|
+
dbg.run_until("MAX(SEQREC)") # ...or up to the step whose command has that text
|
|
150
|
+
dbg.jump_to(line=40) # ...or position by file line; find_step() returns the number
|
|
151
|
+
dbg.show_vars() # state of every variable (OUTPUT params included)
|
|
152
|
+
dbg.eval("@a * @b") # evaluate one T-SQL expression with the CURRENT variables
|
|
153
|
+
dbg.stack() # where am I? procedures + expanded blocks + iteration
|
|
154
|
+
dbg.sql("SELECT COUNT(*) FROM dbo.movements") # query on the SAME session
|
|
155
|
+
dbg.jump_to(17) # move the cursor without running earlier steps
|
|
156
|
+
dbg.set_var("@sqlSrc", "...") # build state by hand
|
|
157
|
+
dbg.run_step(17) # run ONLY step 17 (does not move the cursor)
|
|
158
|
+
dbg.set_log_level("full") # full command + SQL batch on errors
|
|
159
|
+
dbg.show_detail() # last step in full (command + error + batch)
|
|
160
|
+
dbg.show_error() # the step that FAILED, in full — the one-call idiom
|
|
161
|
+
# after a failed run_all() (last_error() for the dict)
|
|
162
|
+
dbg.last_results() # result sets the procedure itself produced
|
|
163
|
+
dbg.watch("(SELECT COUNT(*) FROM stg.movements)", "stg") # tracked every step
|
|
164
|
+
dbg.log_at(8, "@fat") # logpoint: print the value there, never stop
|
|
165
|
+
dbg.clear_logpoints() # remove one logpoint (by line) or all
|
|
166
|
+
dbg.break_at(42, "@code = 31000") # run_all() stops there when it's true
|
|
167
|
+
dbg.break_at(8, hits=4) # ...or from the 4th pass on (once=True: fire once)
|
|
168
|
+
dbg.save_state("st.json") # variables snapshot (JSON) ...
|
|
169
|
+
dbg.load_state("st.json") # ... resume tomorrow with jump_to()
|
|
170
|
+
dbg.reset() # rollback + replay from step 1 on a fresh session
|
|
171
|
+
dbg.close() # ROLLBACK and close (commit=True to persist)
|
|
172
|
+
|
|
173
|
+
from tsql_fabric_debugger import diff_logs
|
|
174
|
+
diff_logs(log_2015, log_2016) # divergences between two runs
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Each step shows the line in the original file, the variables that changed
|
|
178
|
+
and, on errors, the clean SQL Server message:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
[ 5] line 19 | ok | SELECT @offset = ISNULL(MAX(id), 0) FROM dbo.movements
|
|
182
|
+
-> @offset = 184230
|
|
183
|
+
[ 6] line 21 | ok | condition: @year < 2018
|
|
184
|
+
-> condition = True
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Batch mode and CLI
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from tsql_fabric_debugger import run_procedure, summarize
|
|
191
|
+
|
|
192
|
+
log = run_procedure("prd_load.sql", params={"@year": 2015},
|
|
193
|
+
server=..., database=..., save_csv="log.csv")
|
|
194
|
+
summarize(log) # "OK — all N steps ran" or "FAILED at step X (line Y): <msg>"
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`summarize(log)` is the "just tell me what happened" verdict — it prints one
|
|
198
|
+
line and returns the facts (`ok`, `error_step`, `error_line`, `error`,
|
|
199
|
+
`handled`), so you rarely need the step-by-step API for a quick check.
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
tsql-debug prd_load.sql --param @year=2015 \
|
|
203
|
+
--server <endpoint> --database my_warehouse --csv log.csv
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Server and database can also come from the `FABRIC_TSQL_SERVER` and
|
|
207
|
+
`FABRIC_TSQL_DATABASE` environment variables. Files without a
|
|
208
|
+
`CREATE PROCEDURE` go through `run_script()` — split on `GO` lines when
|
|
209
|
+
present, otherwise per statement via the library's own scanner (a `;` inside
|
|
210
|
+
a string never splits) — also inside a transaction with ROLLBACK by default.
|
|
211
|
+
On the CLI, `--param` values can be quoted (`--param @code='00123'`) to force
|
|
212
|
+
a string and keep leading zeros; unquoted values infer int/float strictly
|
|
213
|
+
(no scientific notation, no `nan`/`inf`). Exit codes: 0 = clean, 1 = at
|
|
214
|
+
least one step recorded an ERROR (even if the procedure's CATCH handled it),
|
|
215
|
+
2 = usage/file error. `.sql` files may be UTF-8 (with or without BOM),
|
|
216
|
+
UTF-16 with BOM (SSMS default) or cp1252.
|
|
217
|
+
|
|
218
|
+
### Constructor parameters
|
|
219
|
+
|
|
220
|
+
| Parameter | Default | Purpose |
|
|
221
|
+
|---|---|---|
|
|
222
|
+
| `params` | `{}` | test values for procedure parameters (`{"@year": 2015}`) |
|
|
223
|
+
| `autocommit` | `False` | `True` = every step persists immediately (a warning is echoed; `close()` undoes nothing) |
|
|
224
|
+
| `log_level` | `"simple"` | `"full"` prints whole commands, untruncated variables and the SQL batch on errors |
|
|
225
|
+
| `stop_on_error` | `True` | stop the sequential run on an unhandled error (`False`: continue past errors; `"any"`: also pause on CATCH-handled errors) |
|
|
226
|
+
| `step_timeout` | `None` | per-step query timeout in seconds (`None` = unlimited) |
|
|
227
|
+
| `lock_timeout` | `None` | seconds to wait for a lock before failing (error 1222) instead of hanging behind another session — the anti-hang for orphaned-transaction locks; does not prevent the orphan, only bounds the wait |
|
|
228
|
+
| `max_result_rows` | `50` | rows captured per result set the procedure produces |
|
|
229
|
+
| `max_loop_iterations` | `1000` | guard for `step_into()` on WHILE loops |
|
|
230
|
+
| `preview_chars` | `500` | command truncation in the log's `command` column |
|
|
231
|
+
| `offload_threshold` | `200_000` | strings above this length are kept in a server-side session table and hydrated per batch (uploaded once per change) instead of re-sent on every step |
|
|
232
|
+
| `history_batches` | `None` | keep the heavy per-step payloads (batch text, result sets) only for the last N entries — ERROR entries always keep everything |
|
|
233
|
+
| `echo` | `print` | console output sink |
|
|
234
|
+
|
|
235
|
+
### Log columns
|
|
236
|
+
|
|
237
|
+
`log_df()` / `--csv` (procedure mode): `step`, `line` (file line), `kind`
|
|
238
|
+
(`stmt`/`declare`/`if_block`/`while_block`/`cond`/`exec`/`eval`/`return`/`throw`/`params`),
|
|
239
|
+
`status` (`SUCCESS`/`ERROR`/`REGISTERED`), `rows_affected` (only for captured
|
|
240
|
+
steps; `None` otherwise), `duration_s`, `command` (truncated preview),
|
|
241
|
+
`changed_vars` (truncated — `show_detail()` has the full values),
|
|
242
|
+
`result_sets`, `post_rollback` (True for steps that ran after an
|
|
243
|
+
error-triggered rollback), `error`. The script mode (`run_script`) logs a
|
|
244
|
+
smaller schema: `step`, `status`, `rows_affected`, `duration_s`, `command`,
|
|
245
|
+
`error`.
|
|
246
|
+
|
|
247
|
+
## Security
|
|
248
|
+
|
|
249
|
+
- **Entra ID** authentication only: the notebook token on Fabric,
|
|
250
|
+
`AzureCliCredential` elsewhere. No passwords, ever.
|
|
251
|
+
- Execution inside a transaction with ROLLBACK by default;
|
|
252
|
+
`close(commit=True)` is an explicit decision.
|
|
253
|
+
- Variable re-injection through pyodbc parameters — no value concatenation
|
|
254
|
+
into SQL.
|
|
255
|
+
- Only debug files you trust: the `.sql` **is** code executed under your
|
|
256
|
+
identity, and rollback-by-default is a convenience, not a security
|
|
257
|
+
boundary (a `COMMIT` hidden in dynamic SQL persists — the parser warns
|
|
258
|
+
about literal and string-embedded transaction control, and flags `EXEC`
|
|
259
|
+
calls it cannot see into).
|
|
260
|
+
- Console output and CSV logs contain **real data** from the warehouse
|
|
261
|
+
(variable values, result-set rows) — treat them like the data itself.
|
|
262
|
+
|
|
263
|
+
## Operational notes (read before debugging a shared warehouse)
|
|
264
|
+
|
|
265
|
+
- **Required permissions**: the debugger does NOT `EXECUTE` the procedure —
|
|
266
|
+
it runs the body's statements directly under your identity. You need
|
|
267
|
+
SELECT/INSERT/UPDATE/DELETE on every object the procedure touches
|
|
268
|
+
(ownership chaining does not apply), and row-level security/column masks
|
|
269
|
+
apply to *you*, which can make results diverge from a real execution.
|
|
270
|
+
- **Long transactions hold locks**: the debug session keeps one transaction
|
|
271
|
+
open from the first step until `close()`. Locks from completed steps are
|
|
272
|
+
retained the whole time — an interactive session parked for an hour blocks
|
|
273
|
+
concurrent writers and DDL on the touched tables. Debug in a dev
|
|
274
|
+
warehouse/schema, use the `with` form, and `close()` as soon as you are
|
|
275
|
+
done. `step_timeout` bounds a *running* statement only.
|
|
276
|
+
- The session identifies itself as `tsql-fabric-debugger` in
|
|
277
|
+
`sys.dm_exec_sessions.program_name`.
|
|
278
|
+
- Instances are **not thread-safe** (one session, one shared environment).
|
|
279
|
+
- Memory: the full text of every executed batch is kept for `show_detail()`;
|
|
280
|
+
very long sessions over procedures with multi-MB dynamic SQL grow
|
|
281
|
+
accordingly.
|
|
282
|
+
|
|
283
|
+
## Semantics preserved
|
|
284
|
+
|
|
285
|
+
- `RETURN` ends the debug wherever it appears — top level, guard clause
|
|
286
|
+
inside an `IF`, or inside the emulated `CATCH` — just as it would end the
|
|
287
|
+
real execution.
|
|
288
|
+
- Each `BEGIN TRY` gets **its own** emulated `CATCH`; after the CATCH handles
|
|
289
|
+
the error, the debug skips the rest of that TRY (nested TRY blocks
|
|
290
|
+
included) and continues after `END CATCH` — the same T-SQL semantics. A
|
|
291
|
+
`THROW` inside the CATCH aborts the debug like the real re-raise would.
|
|
292
|
+
- The full `ERROR_*()` family works in the emulated CATCH: `ERROR_MESSAGE()`,
|
|
293
|
+
`ERROR_NUMBER()`, `ERROR_PROCEDURE()`, `ERROR_LINE()` (the file line of the
|
|
294
|
+
failing step), plus `ERROR_SEVERITY()`/`ERROR_STATE()` as RAISERROR-style
|
|
295
|
+
defaults (16/1) — the driver does not expose the real ones.
|
|
296
|
+
- `DECLARE` inside the CATCH (the classic `DECLARE @msg = ERROR_MESSAGE();`)
|
|
297
|
+
is emulated correctly.
|
|
298
|
+
- `IF x SET a = 1 ELSE SET a = 2` — no `;` before the `ELSE` — parses and
|
|
299
|
+
steps correctly.
|
|
300
|
+
- Bodies without an outer `BEGIN...END` (bare statements, or starting
|
|
301
|
+
straight at `BEGIN TRY`) are supported.
|
|
302
|
+
- Session `SET` options (`NOCOUNT`, `XACT_ABORT`, ...) run unparameterized so
|
|
303
|
+
they persist for the following steps, as they would in a real execution.
|
|
304
|
+
- `SELECT`s produced by the procedure itself (diagnostics, samples) are
|
|
305
|
+
captured and displayed (`last_results()`), not discarded — including the
|
|
306
|
+
ones produced before a step failed.
|
|
307
|
+
- Procedures with inner `COMMIT`/`BEGIN TRAN` raise a parse-time warning —
|
|
308
|
+
including transaction keywords spotted **inside string literals** (dynamic
|
|
309
|
+
SQL); `EXEC` calls are flagged as opaque.
|
|
310
|
+
- After an error-triggered rollback, the debug warns that following steps run
|
|
311
|
+
against post-rollback data, and marks them with `post_rollback=True` in the
|
|
312
|
+
log.
|
|
313
|
+
- `step_timeout=<seconds>` in the constructor bounds every step; Ctrl+C
|
|
314
|
+
cancels the running statement server-side and rolls back.
|
|
315
|
+
|
|
316
|
+
## Practical limits
|
|
317
|
+
|
|
318
|
+
- **Table variables**: `DECLARE @t TABLE (...)` is declared in every batch
|
|
319
|
+
(references compile) and triggers a warning, but its **content does not
|
|
320
|
+
survive across steps** — step over the block that fills and consumes it,
|
|
321
|
+
or use `#temp`.
|
|
322
|
+
- `step_into` on a `WHILE` with `BREAK`/`CONTINUE` falls back to atomic mode
|
|
323
|
+
(with a warning).
|
|
324
|
+
- `GOTO`, cursors and `WAITFOR` are out of scope.
|
|
325
|
+
- A single failing statement keeps the previous variable values (same T-SQL
|
|
326
|
+
semantics); when an atomic block fails, use `jump_to(n)` + `step_into()`
|
|
327
|
+
to pinpoint the exact statement. After an error, the ROLLBACK undoes the
|
|
328
|
+
data effects of earlier steps, but the captured variables remain.
|
|
329
|
+
|
|
330
|
+
## Tests
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
pytest # unit (offline, no warehouse)
|
|
334
|
+
FABRIC_TSQL_SERVER=... FABRIC_TSQL_DATABASE=... pytest -m integration
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Mutation testing
|
|
338
|
+
|
|
339
|
+
Suite quality is measured with [mutmut](https://mutmut.readthedocs.io/):
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
pip install "tsql-fabric-debugger[dev]"
|
|
343
|
+
mutmut run # ~1,700 mutants over scanner.py and parser.py
|
|
344
|
+
mutmut results # survivors; `mutmut show <id>` prints the diff
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The scope covers `scanner`, `parser` **and `engine`** — the engine is
|
|
348
|
+
exercised offline through a programmable fake pyodbc session
|
|
349
|
+
(`tests/conftest.py`), so its logic (state capture, error routing, CATCH
|
|
350
|
+
emulation, breakpoints, nested EXEC, offload) is mutation-tested without a
|
|
351
|
+
warehouse. `connection`/`runner`/`cli` stay out (thin driver/warehouse
|
|
352
|
+
glue). `tests/test_mutation_hardening.py` pins exact parser/scanner behavior
|
|
353
|
+
and `tests/test_engine_offline.py` drives the engine; the remaining
|
|
354
|
+
survivors are dominated by equivalent mutants.
|
|
355
|
+
|
|
356
|
+
## License
|
|
357
|
+
|
|
358
|
+
MIT © RedRex
|