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.
Files changed (29) hide show
  1. tsql_fabric_debugger-0.3.2/.gitignore +13 -0
  2. tsql_fabric_debugger-0.3.2/CHANGELOG.md +253 -0
  3. tsql_fabric_debugger-0.3.2/LICENSE +21 -0
  4. tsql_fabric_debugger-0.3.2/PKG-INFO +358 -0
  5. tsql_fabric_debugger-0.3.2/README.md +316 -0
  6. tsql_fabric_debugger-0.3.2/docs/DOCUMENTATION.md +664 -0
  7. tsql_fabric_debugger-0.3.2/pyproject.toml +86 -0
  8. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/__init__.py +36 -0
  9. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/cli.py +126 -0
  10. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/connection.py +147 -0
  11. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/dap.py +661 -0
  12. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/engine.py +2076 -0
  13. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/introspect.py +104 -0
  14. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/parser.py +691 -0
  15. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/py.typed +0 -0
  16. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/runner.py +242 -0
  17. tsql_fabric_debugger-0.3.2/src/tsql_fabric_debugger/scanner.py +83 -0
  18. tsql_fabric_debugger-0.3.2/tests/conftest.py +212 -0
  19. tsql_fabric_debugger-0.3.2/tests/fixtures/demo_proc.sql +43 -0
  20. tsql_fabric_debugger-0.3.2/tests/test_cli.py +50 -0
  21. tsql_fabric_debugger-0.3.2/tests/test_dap_offline.py +521 -0
  22. tsql_fabric_debugger-0.3.2/tests/test_engine.py +323 -0
  23. tsql_fabric_debugger-0.3.2/tests/test_engine_offline.py +1310 -0
  24. tsql_fabric_debugger-0.3.2/tests/test_features_02.py +220 -0
  25. tsql_fabric_debugger-0.3.2/tests/test_integration.py +725 -0
  26. tsql_fabric_debugger-0.3.2/tests/test_mutation_hardening.py +341 -0
  27. tsql_fabric_debugger-0.3.2/tests/test_parser.py +235 -0
  28. tsql_fabric_debugger-0.3.2/tests/test_runner.py +38 -0
  29. tsql_fabric_debugger-0.3.2/tests/test_scanner.py +36 -0
@@ -0,0 +1,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ dist/
7
+ build/
8
+ .pytest_cache/
9
+ mutants/
10
+ .mypy_cache/
11
+ .ruff_cache/
12
+ *.csv
13
+ .DS_Store
@@ -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