fix: bug-bash batch 2 — six small correctness/hygiene fixes#250
fix: bug-bash batch 2 — six small correctness/hygiene fixes#250erikdarlingdata merged 1 commit intodevfrom
Conversation
- ReproScriptBuilder: double ']' in database names before emitting USE, so identifiers like `cool]stuff` produce valid T-SQL instead of unbalanced brackets. - ReproScriptBuilder: whitelist SET TRANSACTION ISOLATION LEVEL against the five valid T-SQL names; unknown/hostile strings drop silently instead of interpolating into the script. - ShowPlanParser.ParseLong: pass NumberStyles.Integer + InvariantCulture to match sibling ParseDouble; makes numeric parsing culture-independent. - KeychainCredentialService: read stdout and stderr concurrently via Task.WhenAll to avoid the classic "sync stderr while stdout is async" deadlock, which could surface on `security dump-keychain` with large keychains. - QueryStoreHistoryWindow: override OnClosed to cancel + dispose the in-flight fetch CancellationTokenSource, so SqlConnection doesn't sit open on the server when the dialog is closed mid-load. - QuerySessionControl: on DetachedFromVisualTree, cancel + dispose _statusClearCts alongside the existing TextMate teardown, so the fire-and-forget status-clear dispatch can't touch a dead control. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
erikdarlingdata
left a comment
There was a problem hiding this comment.
Review
What it does: Bug-bash batch 2 — five files, six narrow fixes: ]-doubling for USE [db], whitelist for SET TRANSACTION ISOLATION LEVEL, culture-invariant ParseLong, deadlock fix in security subprocess I/O, and cancel+dispose of two CancellationTokenSource fields on window close / control detach.
Good:
- Targets
dev✓ - Isolation-level whitelist uses the exact T-SQL-spec set with
StringComparer.Ordinal+ToUpperInvariant()— correct. ]→]]doubling is the correct escape for bracket-quoted identifiers.ParseLongnow matches theParseDoublesibling (NumberStyles.Integer+InvariantCulture) — no morede-DEsurprises.- Keychain deadlock fix is real; switching stderr from sync
ReadToEndto async solves the classic pipe-buffer deadlock. - Both CTS-cleanup fixes (
QueryStoreHistoryWindow.OnClosed,QuerySessionControl.DetachedFromVisualTree) follow the cancel-then-dispose-then-null pattern correctly.
Needs attention (non-blocking, none regress existing behavior):
QueryStoreHistoryWindow.LoadHistoryAsyncstill has a post-await UI-write path that doesn't checkct.IsCancellationRequested. If the fetch returns betweenOnClosedandawaitresumption,HistoryDataGrid.ItemsSource = …runs on a closed window. Pre-existing; flagged since you're touching this lifecycle.KeychainCredentialService.RunSecurityordering: Microsoft's documented pattern isTask.WhenAll(reads); WaitForExit();rather thanWaitForExit(); WaitAll(reads);. Works either way with both streams async, but worth aligning.ReproScriptBuilderwhitelist +]-escape are both new security-hardening surface area with no coverage intests/PlanViewer.Core.Tests/. Two xunit cases would pin the hostile-string-drops and bracket-escape behaviors.
Avalonia / theming: No AvaloniaEdit, no PlaceholderText/Watermark, no :pointerover selectors, no new brushes or MVVM creep — nothing to flag.
PlanAnalyzer: Not touched. No Dashboard/Lite sync required.
Noted this was merged seven seconds after opening — comments are for the record / follow-up.
Generated by Claude Code
| _fetchCts?.Dispose(); | ||
| _fetchCts = null; | ||
| base.OnClosed(e); | ||
| } |
There was a problem hiding this comment.
Pre-existing, not introduced here, but worth flagging while you're in this file: LoadHistoryAsync (lines 191–239) catches OperationCanceledException but the success path still touches StatusText, HistoryDataGrid.ItemsSource, UpdateChart(), and PopulateLegendPanel() without checking ct.IsCancellationRequested. If the fetch returns normally in the tiny window between OnClosed firing and the await resuming, those UI writes happen on a closed window. Consider a ct.ThrowIfCancellationRequested() after the await (or before the UI writes) so the post-await path routes through the OperationCanceledException catch.
Generated by Claude Code
| process.WaitForExit(); | ||
| var stdout = stdoutTask.Result; | ||
| Task.WaitAll(stdoutTask, stderrTask); | ||
|
|
There was a problem hiding this comment.
Minor: Task.WaitAll(stdoutTask, stderrTask) after process.WaitForExit() is redundant — once the process has exited the pipes are closed and both ReadToEndAsync tasks will already be complete. Not a bug (it does guarantee exceptions surface rather than being lost on the task), just noting it. The Microsoft docs actually recommend the opposite order: start async reads, then WaitForExit (or WaitForExitAsync) — because WaitForExit without draining can itself block indefinitely in some shell scenarios. Current ordering works because both reads are async, but flipping to await Task.WhenAll(stdoutTask, stderrTask); process.WaitForExit(); would be the textbook pattern.
Generated by Claude Code
| "READ COMMITTED", | ||
| "REPEATABLE READ", | ||
| "SNAPSHOT", | ||
| "SERIALIZABLE", |
There was a problem hiding this comment.
Whitelist is correct per T-SQL spec — these are exactly the five values SET TRANSACTION ISOLATION LEVEL accepts. READ COMMITTED SNAPSHOT is deliberately absent and should be, since RCSI is a database-level option (ALTER DATABASE … SET READ_COMMITTED_SNAPSHOT ON) and not a transaction isolation level keyword. 👍
No tests in tests/PlanViewer.Core.Tests/ cover ReproScriptBuilder whitelist or ] escaping behavior — given this is now the security-hardening layer for a code-gen path, a couple of xunit cases pinning the two behaviors (valid level passes through, hostile string is silently dropped; cool]stuff → cool]]stuff) would be cheap insurance against regressions.
Generated by Claude Code
| if (!string.IsNullOrEmpty(databaseName) && !isAzureSqlDb) | ||
| { | ||
| sb.AppendLine($"USE [{databaseName}];"); | ||
| sb.AppendLine($"USE [{databaseName.Replace("]", "]]")}];"); |
There was a problem hiding this comment.
Escape is correct for the bracket-quoted identifier form. If anywhere else in this builder emits the database name without [...] brackets (e.g. as a plain token or inside a single-quoted string for sp_executesql), the ]→]] doubling alone isn't the right escape for those forms. A quick grep for other databaseName usages in this file confirms whether this is the only emission site.
Generated by Claude Code
| if (string.IsNullOrEmpty(value)) return 0; | ||
| return long.TryParse(value, out var result) ? result : 0; | ||
| return long.TryParse(value, System.Globalization.NumberStyles.Integer, | ||
| System.Globalization.CultureInfo.InvariantCulture, out var result) ? result : 0; |
There was a problem hiding this comment.
Good alignment with ParseDouble. PR body notes the overflow case (99999…) still returns 0 — that's fine for ShowPlan's invariant-formatted payloads, just worth noting that silent-zero-on-overflow could mask a malformed plan. Not worth changing in this PR.
Generated by Claude Code
Summary
Batch 2 from the bug-bash review — bundles six fixes that are all a few lines each, no feature changes.
ReproScriptBuilder.cs]in DB names soUSE [cool]]stuff]is well-formed instead of unbalanced.ReproScriptBuilder.csSET TRANSACTION ISOLATION LEVELagainst the five valid T-SQL names; unknown/hostile strings drop silently.ShowPlanParser.csParseLongnow passesNumberStyles.Integer+InvariantCulture, matching the siblingParseDouble.KeychainCredentialService.cssecurity dump-keychainwith large keychains).QueryStoreHistoryWindow.axaml.csOnClosedoverride cancels + disposes_fetchCtsso closing mid-load doesn't leave the SqlConnection open on the server.QuerySessionControl.axaml.csDetachedFromVisualTreealso cancels_statusClearCtsso the fire-and-forget status-clear dispatch can't touch a detached control.Test plan
dotnet build PlanViewer.sln— 0 errors.databaseName = "cool]stuff"→ emitsUSE [cool]]stuff];✅databaseName = "NormalDb"→ emitsUSE [NormalDb];(regression check) ✅"read committed"→ emitsSET TRANSACTION ISOLATION LEVEL READ COMMITTED;✅"READ COMMITTED; DROP DATABASE [prod];--"→ no SET line ✅"CHAOS"→ no SET line ✅de-DE, invariant-digit string"123456789012345"parses correctly ✅"99999999999999999999") still returns0— documented limitation, deliberately not changed in this PR to keep the diff tight.🤖 Generated with Claude Code