|
| 1 | +using System.Linq; |
| 2 | +using PlanViewer.Core.Output; |
| 3 | +using PlanViewer.Core.Services; |
| 4 | + |
| 5 | +namespace PlanViewer.Core.Tests; |
| 6 | + |
| 7 | +/// <summary> |
| 8 | +/// #455: a plan captured around <c>EXEC <procedure></c> analyzed as one statement, zero |
| 9 | +/// warnings, zero cost, exit 0 — on a file containing dozens of statement plans. |
| 10 | +/// |
| 11 | +/// <para>The reported diagnosis was that the parse never descended into the procedure. It is subtler |
| 12 | +/// than that and the distinction decides the fix: the parser had always read <c>StoredProc</c> |
| 13 | +/// sub-plans, but that code sat BELOW an early return taken when a statement carries no |
| 14 | +/// <c>QueryPlan</c> of its own — which is exactly what an <c>EXEC</c> statement is. The descent |
| 15 | +/// existed and was unreachable in the only case it was written for.</para> |
| 16 | +/// |
| 17 | +/// <para>What made it dangerous is that the output was well-formed and plausible. Nothing said the |
| 18 | +/// analysis had stopped early; it looked like a clean plan with nothing to report, which invites |
| 19 | +/// "no warnings found" about a procedure that in fact has plenty.</para> |
| 20 | +/// </summary> |
| 21 | +public class StoredProcedurePlanTests |
| 22 | +{ |
| 23 | + /// <summary> |
| 24 | + /// The three numbers from the report, asserted together. Any one alone could be innocent; the |
| 25 | + /// combination — one statement, no warnings, no cost — is the signature of a parse that did not |
| 26 | + /// reach the real statements. |
| 27 | + /// </summary> |
| 28 | + [Fact] |
| 29 | + public void AnExecProcedurePlanAnalyzesTheProcedureBody() |
| 30 | + { |
| 31 | + var plan = PlanTestHelper.LoadAndAnalyze("exec_stored_procedure_plan.sqlplan"); |
| 32 | + var result = ResultMapper.Map(plan, "exec_stored_procedure_plan.sqlplan"); |
| 33 | + |
| 34 | + Assert.True(result.Summary.TotalStatements > 1, |
| 35 | + $"the procedure body's statements are missing (got {result.Summary.TotalStatements})"); |
| 36 | + Assert.True(result.Summary.TotalWarnings > 0, |
| 37 | + "a body with a non-SARGable predicate and a table variable cannot be warning-free"); |
| 38 | + Assert.True(result.Summary.MaxEstimatedCost > 0, |
| 39 | + "every statement carrying a plan has a cost; zero means none were seen"); |
| 40 | + } |
| 41 | + |
| 42 | + /// <summary> |
| 43 | + /// The specific cause, pinned so a future edit cannot reintroduce it by moving sub-plan parsing |
| 44 | + /// back below the early return: the EXEC statement itself has no QueryPlan, and must still |
| 45 | + /// carry its body. |
| 46 | + /// </summary> |
| 47 | + [Fact] |
| 48 | + public void TheExecStatementHasNoPlanOfItsOwnAndStillCarriesItsBody() |
| 49 | + { |
| 50 | + var plan = PlanTestHelper.LoadAndAnalyze("exec_stored_procedure_plan.sqlplan"); |
| 51 | + |
| 52 | + var exec = plan.Batches.SelectMany(b => b.Statements).Single(); |
| 53 | + |
| 54 | + Assert.NotNull(exec.StoredProcPlan); |
| 55 | + Assert.NotEmpty(exec.StoredProcPlan!.Statements); |
| 56 | + } |
| 57 | + |
| 58 | + /// <summary> |
| 59 | + /// The traversal the analyzer, the mapper and the test helper now share. They each walked |
| 60 | + /// batch.Statements separately before, which is how the analyzer and the golden master came to |
| 61 | + /// have the same blind spot and neither could catch the other. |
| 62 | + /// </summary> |
| 63 | + [Fact] |
| 64 | + public void EnumerateAllReachesNestedStatements() |
| 65 | + { |
| 66 | + var plan = PlanTestHelper.LoadAndAnalyze("exec_stored_procedure_plan.sqlplan"); |
| 67 | + |
| 68 | + var shallow = plan.Batches.SelectMany(b => b.Statements).Count(); |
| 69 | + var all = PlanStatements.EnumerateAll(plan).Count(); |
| 70 | + |
| 71 | + Assert.Equal(1, shallow); |
| 72 | + Assert.True(all > shallow, $"nested statements were not reached (shallow {shallow}, all {all})"); |
| 73 | + } |
| 74 | + |
| 75 | + /// <summary> |
| 76 | + /// The outer statement comes back before its body. Order matters for output: a reader scanning |
| 77 | + /// the statement list should meet the EXEC before the statements it caused. |
| 78 | + /// </summary> |
| 79 | + [Fact] |
| 80 | + public void TheCallingStatementComesBeforeItsBody() |
| 81 | + { |
| 82 | + var plan = PlanTestHelper.LoadAndAnalyze("exec_stored_procedure_plan.sqlplan"); |
| 83 | + |
| 84 | + var all = PlanStatements.EnumerateAll(plan).ToList(); |
| 85 | + var exec = plan.Batches.SelectMany(b => b.Statements).Single(); |
| 86 | + |
| 87 | + Assert.Same(exec, all[0]); |
| 88 | + } |
| 89 | + |
| 90 | + /// <summary> |
| 91 | + /// Plans without a procedure are untouched. The fix only ever adds statements that were being |
| 92 | + /// dropped, so a plain batch must enumerate exactly as it always did. |
| 93 | + /// </summary> |
| 94 | + [Theory] |
| 95 | + [InlineData("row_goal_plan.sqlplan")] |
| 96 | + [InlineData("key_lookup_plan.sqlplan")] |
| 97 | + [InlineData("spill_plan.sqlplan")] |
| 98 | + public void APlanWithNoProcedureEnumeratesUnchanged(string planFile) |
| 99 | + { |
| 100 | + var plan = PlanTestHelper.LoadAndAnalyze(planFile); |
| 101 | + |
| 102 | + Assert.Equal( |
| 103 | + plan.Batches.SelectMany(b => b.Statements).Count(), |
| 104 | + PlanStatements.EnumerateAll(plan).Count()); |
| 105 | + } |
| 106 | +} |
0 commit comments