refactor(config-db): GetTraceEventsAsync and GetExecutionTreeAsync share one chain walker (arch-review F9)
GetTraceEventsAsync re-implemented GetExecutionTreeAsync's execution-chain walk as a second copy: the upward ParentExecutionId climb, the root MIN(OccurredAtUtc) window, and the recursive Edges/Chain CTE with its DISTINCT-edges trick and MAXRECURSION cap. Two walkers that had to stay in step on window slack, depth and the edge distillation. Extracted into three shared members: - ChainAnchor (root execution id + root's first event timestamp, with the Edges window predicate derived from it) - ResolveChainAnchorAsync — phase 1 up-walk plus the root window read - ChainCtePrefix / AddChainParameters — the Edges/Chain CTE prefix and its @root/@winStart/@winEnd binding Each walker appends its own projection to the prefix: GetExecutionTreeAsync the ChainIds CTE plus the per-execution aggregate, GetTraceEventsAsync the bare id union. Both keep OPTION (MAXRECURSION 32). Pure refactor. The tree walker's generated SQL is byte-identical (verified by diffing the composed command text against the pre-change literal); the trace walker's differs only by the SQL comment lines it now inherits from the shared prefix. Behaviour is pinned by the existing MSSQL-backed tests (GetExecutionTree_MultiLevelChain / _StubParentNode / _RespectsMaxRecursion, ExecutionTree_Bounds_EdgeScan_To_Root_Window, and both AuditLogRepositoryTraceEventsTests) — 410 passed / 0 skipped before and after. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
+166
-173
@@ -1,4 +1,5 @@
|
|||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Data.Common;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -918,105 +919,114 @@ VALUES
|
|||||||
private static readonly TimeSpan ExecutionTreeWindowSlack = TimeSpan.FromHours(1);
|
private static readonly TimeSpan ExecutionTreeWindowSlack = TimeSpan.FromHours(1);
|
||||||
private static readonly TimeSpan ExecutionTreeMaxSpan = TimeSpan.FromDays(7);
|
private static readonly TimeSpan ExecutionTreeMaxSpan = TimeSpan.FromDays(7);
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <summary>
|
||||||
public async Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
|
/// The anchor of one execution chain: the root execution id reached by the upward
|
||||||
Guid executionId,
|
/// <c>ParentExecutionId</c> walk, plus that root's first event timestamp (null when the
|
||||||
CancellationToken ct = default)
|
/// root is a row-less stub). Produced by <see cref="ResolveChainAnchorAsync"/> and
|
||||||
|
/// consumed by <see cref="ChainCtePrefix"/> / <see cref="AddChainParameters"/>.
|
||||||
|
/// </summary>
|
||||||
|
private sealed record ChainAnchor(Guid RootExecutionId, DateTime? RootFirstOccurred)
|
||||||
{
|
{
|
||||||
var conn = _context.Database.GetDbConnection();
|
/// <summary>
|
||||||
var openedHere = false;
|
/// The optional window predicate for the Edges CTE. When the root has rows the scan is
|
||||||
if (conn.State != System.Data.ConnectionState.Open)
|
/// confined to the window (partition elimination); otherwise it stays unbounded.
|
||||||
|
/// </summary>
|
||||||
|
public string EdgesWindowPredicate => RootFirstOccurred is null
|
||||||
|
? string.Empty
|
||||||
|
: "AND OccurredAtUtc >= @winStart AND OccurredAtUtc < @winEnd";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 1 of the execution-chain walk, shared by <see cref="GetExecutionTreeAsync"/> and
|
||||||
|
/// <see cref="GetTraceEventsAsync"/> (arch-review F9): climb to the chain root, then read the
|
||||||
|
/// root's first event timestamp so the down-walk can be bounded to a root-anchored window.
|
||||||
|
/// The two walkers must stay in step on window slack, depth cap and the DISTINCT-edges trick,
|
||||||
|
/// so there is exactly one copy of each.
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<ChainAnchor> ResolveChainAnchorAsync(
|
||||||
|
DbConnection conn,
|
||||||
|
Guid executionId,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
// --- Phase 1: walk up to the root ---------------------------------
|
||||||
|
// Climb ParentExecutionId until a node has no parent (root) or the
|
||||||
|
// parent execution has no rows of its own (purged/stub — the climb
|
||||||
|
// cannot continue past a row-less node). The depth cap guards
|
||||||
|
// against a cycle in corrupt data; a tree never reaches it.
|
||||||
|
var rootExecutionId = executionId;
|
||||||
|
for (var depth = 0; depth < ExecutionChainMaxDepth; depth++)
|
||||||
{
|
{
|
||||||
await conn.OpenAsync(ct).ConfigureAwait(false);
|
Guid? parent;
|
||||||
openedHere = true;
|
await using (var upCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
upCmd.CommandText =
|
||||||
|
"SELECT TOP 1 ParentExecutionId FROM dbo.AuditLog " +
|
||||||
|
"WHERE ExecutionId = @cur AND ParentExecutionId IS NOT NULL;";
|
||||||
|
var pCur = upCmd.CreateParameter();
|
||||||
|
pCur.ParameterName = "@cur";
|
||||||
|
pCur.Value = rootExecutionId;
|
||||||
|
upCmd.Parameters.Add(pCur);
|
||||||
|
|
||||||
|
var result = await upCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
||||||
|
parent = result is null or DBNull ? null : (Guid)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parent is null)
|
||||||
|
{
|
||||||
|
// No parent row for the current node — it is the root (or a
|
||||||
|
// row-less stub at the top of what survives). Stop climbing.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
rootExecutionId = parent.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
// --- Between phases: bound the down-walk to a root-anchored window ---
|
||||||
|
// The root's first event timestamp anchors a [rootFirst - slack, rootFirst + maxSpan)
|
||||||
|
// window used to prune the Edges CTE scan to a handful of partitions. A row-less stub
|
||||||
|
// root (purged/no-action parent) has no MIN — leave the scan unbounded in that
|
||||||
|
// degenerate case (correctness over speed).
|
||||||
|
DateTime? rootFirstOccurred;
|
||||||
|
await using (var winCmd = conn.CreateCommand())
|
||||||
{
|
{
|
||||||
// --- Phase 1: walk up to the root ---------------------------------
|
winCmd.CommandText =
|
||||||
// Climb ParentExecutionId until a node has no parent (root) or the
|
"SELECT MIN(OccurredAtUtc) FROM dbo.AuditLog WHERE ExecutionId = @root;";
|
||||||
// parent execution has no rows of its own (purged/stub — the climb
|
var pWinRoot = winCmd.CreateParameter();
|
||||||
// cannot continue past a row-less node). The depth cap guards
|
pWinRoot.ParameterName = "@root";
|
||||||
// against a cycle in corrupt data; a tree never reaches it.
|
pWinRoot.Value = rootExecutionId;
|
||||||
var rootExecutionId = executionId;
|
winCmd.Parameters.Add(pWinRoot);
|
||||||
for (var depth = 0; depth < ExecutionChainMaxDepth; depth++)
|
|
||||||
{
|
|
||||||
Guid? parent;
|
|
||||||
await using (var upCmd = conn.CreateCommand())
|
|
||||||
{
|
|
||||||
upCmd.CommandText =
|
|
||||||
"SELECT TOP 1 ParentExecutionId FROM dbo.AuditLog " +
|
|
||||||
"WHERE ExecutionId = @cur AND ParentExecutionId IS NOT NULL;";
|
|
||||||
var pCur = upCmd.CreateParameter();
|
|
||||||
pCur.ParameterName = "@cur";
|
|
||||||
pCur.Value = rootExecutionId;
|
|
||||||
upCmd.Parameters.Add(pCur);
|
|
||||||
|
|
||||||
var result = await upCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
var winResult = await winCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
||||||
parent = result is null or DBNull ? null : (Guid)result;
|
rootFirstOccurred = winResult is null or DBNull ? null : (DateTime)winResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent is null)
|
return new ChainAnchor(rootExecutionId, rootFirstOccurred);
|
||||||
{
|
}
|
||||||
// No parent row for the current node — it is the root (or a
|
|
||||||
// row-less stub at the top of what survives). Stop climbing.
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
rootExecutionId = parent.Value;
|
/// <summary>
|
||||||
}
|
/// Phase 2's Edges/Chain CTE prefix — identical in both walkers (arch-review F9). Callers
|
||||||
|
/// append their own projection: <see cref="GetExecutionTreeAsync"/> the <c>ChainIds</c> CTE
|
||||||
// --- Between phases: bound the down-walk to a root-anchored window ---
|
/// plus the per-execution aggregate, <see cref="GetTraceEventsAsync"/> the bare id union.
|
||||||
// The root's first event timestamp anchors a [rootFirst - slack, rootFirst + maxSpan)
|
/// Both must terminate with <c>OPTION (MAXRECURSION …)</c>.
|
||||||
// window used to prune the Edges CTE scan to a handful of partitions. A row-less stub
|
/// </summary>
|
||||||
// root (purged/no-action parent) has no MIN — leave the scan unbounded in that
|
/// <remarks>
|
||||||
// degenerate case (correctness over speed).
|
/// Edges : a non-recursive, DISTINCT (ExecutionId, ParentExecutionId)
|
||||||
DateTime? rootFirstOccurred;
|
/// edge set distilled from AuditLog. Recursing over edges
|
||||||
await using (var winCmd = conn.CreateCommand())
|
/// instead of raw rows means an execution with N audit rows
|
||||||
{
|
/// contributes ONE recursion path, not N — MAXRECURSION
|
||||||
winCmd.CommandText =
|
/// bounds depth, not per-level width, so the raw-row form
|
||||||
"SELECT MIN(OccurredAtUtc) FROM dbo.AuditLog WHERE ExecutionId = @root;";
|
/// could fan out badly. One edge per execution because all
|
||||||
var pWinRoot = winCmd.CreateParameter();
|
/// rows of an execution share a single ParentExecutionId
|
||||||
pWinRoot.ParameterName = "@root";
|
/// (see the MIN(...) note on the tree walker's projection).
|
||||||
pWinRoot.Value = rootExecutionId;
|
/// Chain : seeded at the root edge, recursively joins each edge whose
|
||||||
winCmd.Parameters.Add(pWinRoot);
|
/// ParentExecutionId is an ExecutionId already in the chain.
|
||||||
|
/// Each edge carries its own ParentExecutionId, so the chain
|
||||||
var winResult = await winCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
/// of edges already surfaces every execution id in the tree
|
||||||
rootFirstOccurred = winResult is null or DBNull ? null : (DateTime)winResult;
|
/// — including a row-less stub parent, which appears as the
|
||||||
}
|
/// ParentExecutionId of its child's edge. No separate
|
||||||
|
/// union-back CTE is needed.
|
||||||
// Build the optional window predicate for the Edges CTE. When the root has rows the
|
/// </remarks>
|
||||||
// scan is confined to the window (partition elimination); otherwise it stays unbounded.
|
private static string ChainCtePrefix(string edgesWindowPredicate) => $@"
|
||||||
var edgesWindowPredicate = rootFirstOccurred is null
|
|
||||||
? string.Empty
|
|
||||||
: "AND OccurredAtUtc >= @winStart AND OccurredAtUtc < @winEnd";
|
|
||||||
|
|
||||||
// --- Phase 2: walk down from the root via a recursive CTE ---------
|
|
||||||
// Edges : a non-recursive, DISTINCT (ExecutionId, ParentExecutionId)
|
|
||||||
// edge set distilled from AuditLog. Recursing over edges
|
|
||||||
// instead of raw rows means an execution with N audit rows
|
|
||||||
// contributes ONE recursion path, not N — MAXRECURSION
|
|
||||||
// bounds depth, not per-level width, so the raw-row form
|
|
||||||
// could fan out badly. One edge per execution because all
|
|
||||||
// rows of an execution share a single ParentExecutionId
|
|
||||||
// (see the MIN(...) note on the final projection).
|
|
||||||
// Chain : seeded at the root edge, recursively joins each edge whose
|
|
||||||
// ParentExecutionId is an ExecutionId already in the chain.
|
|
||||||
// Each edge carries its own ParentExecutionId, so the chain
|
|
||||||
// of edges already surfaces every execution id in the tree
|
|
||||||
// — including a row-less stub parent, which appears as the
|
|
||||||
// ParentExecutionId of its child's edge. No separate
|
|
||||||
// union-back CTE is needed.
|
|
||||||
// Final : collect every distinct execution id reachable from the
|
|
||||||
// chain (each edge's ExecutionId plus its non-null
|
|
||||||
// ParentExecutionId), LEFT JOIN back to AuditLog and
|
|
||||||
// GROUP BY so a stub parent — which owns no edge of its own
|
|
||||||
// because it emitted no rows — still surfaces as a node with
|
|
||||||
// RowCount 0 and NULL aggregates.
|
|
||||||
var nodes = new List<ExecutionTreeNode>();
|
|
||||||
await using (var downCmd = conn.CreateCommand())
|
|
||||||
{
|
|
||||||
downCmd.CommandText = $@"
|
|
||||||
WITH Edges AS (
|
WITH Edges AS (
|
||||||
SELECT DISTINCT ExecutionId, ParentExecutionId
|
SELECT DISTINCT ExecutionId, ParentExecutionId
|
||||||
FROM dbo.AuditLog
|
FROM dbo.AuditLog
|
||||||
@@ -1035,7 +1045,65 @@ VALUES
|
|||||||
SELECT e.ExecutionId, e.ParentExecutionId
|
SELECT e.ExecutionId, e.ParentExecutionId
|
||||||
FROM Edges e
|
FROM Edges e
|
||||||
INNER JOIN Chain c ON e.ParentExecutionId = c.ExecutionId
|
INNER JOIN Chain c ON e.ParentExecutionId = c.ExecutionId
|
||||||
),
|
)";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the parameters the <see cref="ChainCtePrefix"/> statement needs: <c>@root</c> always,
|
||||||
|
/// and the window bounds only when the anchor carries a root timestamp (matching
|
||||||
|
/// <see cref="ChainAnchor.EdgesWindowPredicate"/>).
|
||||||
|
/// </summary>
|
||||||
|
private static void AddChainParameters(DbCommand cmd, ChainAnchor anchor)
|
||||||
|
{
|
||||||
|
var pRoot = cmd.CreateParameter();
|
||||||
|
pRoot.ParameterName = "@root";
|
||||||
|
pRoot.Value = anchor.RootExecutionId;
|
||||||
|
cmd.Parameters.Add(pRoot);
|
||||||
|
|
||||||
|
if (anchor.RootFirstOccurred is { } rootFirst)
|
||||||
|
{
|
||||||
|
var pWinStart = cmd.CreateParameter();
|
||||||
|
pWinStart.ParameterName = "@winStart";
|
||||||
|
pWinStart.Value = rootFirst - ExecutionTreeWindowSlack;
|
||||||
|
cmd.Parameters.Add(pWinStart);
|
||||||
|
|
||||||
|
var pWinEnd = cmd.CreateParameter();
|
||||||
|
pWinEnd.ParameterName = "@winEnd";
|
||||||
|
pWinEnd.Value = rootFirst + ExecutionTreeMaxSpan;
|
||||||
|
cmd.Parameters.Add(pWinEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
|
||||||
|
Guid executionId,
|
||||||
|
CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var conn = _context.Database.GetDbConnection();
|
||||||
|
var openedHere = false;
|
||||||
|
if (conn.State != System.Data.ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await conn.OpenAsync(ct).ConfigureAwait(false);
|
||||||
|
openedHere = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// --- Phase 1 + window: shared with GetTraceEventsAsync (arch-review F9) ---
|
||||||
|
var anchor = await ResolveChainAnchorAsync(conn, executionId, ct).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// --- Phase 2: walk down from the root via a recursive CTE ---------
|
||||||
|
// The Edges/Chain prefix is ChainCtePrefix (see its remarks for the
|
||||||
|
// DISTINCT-edges trick); this walker appends:
|
||||||
|
// ChainIds: collect every distinct execution id reachable from the
|
||||||
|
// chain (each edge's ExecutionId plus its non-null
|
||||||
|
// ParentExecutionId), LEFT JOIN back to AuditLog and
|
||||||
|
// GROUP BY so a stub parent — which owns no edge of its own
|
||||||
|
// because it emitted no rows — still surfaces as a node with
|
||||||
|
// RowCount 0 and NULL aggregates.
|
||||||
|
var nodes = new List<ExecutionTreeNode>();
|
||||||
|
await using (var downCmd = conn.CreateCommand())
|
||||||
|
{
|
||||||
|
downCmd.CommandText = ChainCtePrefix(anchor.EdgesWindowPredicate) + $@",
|
||||||
ChainIds AS (
|
ChainIds AS (
|
||||||
SELECT ExecutionId FROM Chain
|
SELECT ExecutionId FROM Chain
|
||||||
UNION
|
UNION
|
||||||
@@ -1071,23 +1139,7 @@ VALUES
|
|||||||
GROUP BY ids.ExecutionId
|
GROUP BY ids.ExecutionId
|
||||||
OPTION (MAXRECURSION {ExecutionChainMaxDepth});";
|
OPTION (MAXRECURSION {ExecutionChainMaxDepth});";
|
||||||
|
|
||||||
var pRoot = downCmd.CreateParameter();
|
AddChainParameters(downCmd, anchor);
|
||||||
pRoot.ParameterName = "@root";
|
|
||||||
pRoot.Value = rootExecutionId;
|
|
||||||
downCmd.Parameters.Add(pRoot);
|
|
||||||
|
|
||||||
if (rootFirstOccurred is { } rootFirst)
|
|
||||||
{
|
|
||||||
var pWinStart = downCmd.CreateParameter();
|
|
||||||
pWinStart.ParameterName = "@winStart";
|
|
||||||
pWinStart.Value = rootFirst - ExecutionTreeWindowSlack;
|
|
||||||
downCmd.Parameters.Add(pWinStart);
|
|
||||||
|
|
||||||
var pWinEnd = downCmd.CreateParameter();
|
|
||||||
pWinEnd.ParameterName = "@winEnd";
|
|
||||||
pWinEnd.Value = rootFirst + ExecutionTreeMaxSpan;
|
|
||||||
downCmd.Parameters.Add(pWinEnd);
|
|
||||||
}
|
|
||||||
|
|
||||||
await using var reader = await downCmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
await using var reader = await downCmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||||
@@ -1147,77 +1199,18 @@ VALUES
|
|||||||
var chainIds = new List<Guid>();
|
var chainIds = new List<Guid>();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var rootExecutionId = executionId;
|
var anchor = await ResolveChainAnchorAsync(conn, executionId, ct).ConfigureAwait(false);
|
||||||
for (var depth = 0; depth < ExecutionChainMaxDepth; depth++)
|
|
||||||
{
|
|
||||||
Guid? parent;
|
|
||||||
await using (var upCmd = conn.CreateCommand())
|
|
||||||
{
|
|
||||||
upCmd.CommandText =
|
|
||||||
"SELECT TOP 1 ParentExecutionId FROM dbo.AuditLog " +
|
|
||||||
"WHERE ExecutionId = @cur AND ParentExecutionId IS NOT NULL;";
|
|
||||||
var pCur = upCmd.CreateParameter();
|
|
||||||
pCur.ParameterName = "@cur";
|
|
||||||
pCur.Value = rootExecutionId;
|
|
||||||
upCmd.Parameters.Add(pCur);
|
|
||||||
var result = await upCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
|
||||||
parent = result is null or DBNull ? null : (Guid)result;
|
|
||||||
}
|
|
||||||
if (parent is null) break;
|
|
||||||
rootExecutionId = parent.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
DateTime? rootFirstOccurred;
|
|
||||||
await using (var winCmd = conn.CreateCommand())
|
|
||||||
{
|
|
||||||
winCmd.CommandText = "SELECT MIN(OccurredAtUtc) FROM dbo.AuditLog WHERE ExecutionId = @root;";
|
|
||||||
var pWinRoot = winCmd.CreateParameter();
|
|
||||||
pWinRoot.ParameterName = "@root";
|
|
||||||
pWinRoot.Value = rootExecutionId;
|
|
||||||
winCmd.Parameters.Add(pWinRoot);
|
|
||||||
var winResult = await winCmd.ExecuteScalarAsync(ct).ConfigureAwait(false);
|
|
||||||
rootFirstOccurred = winResult is null or DBNull ? null : (DateTime)winResult;
|
|
||||||
}
|
|
||||||
var edgesWindowPredicate = rootFirstOccurred is null
|
|
||||||
? string.Empty
|
|
||||||
: "AND OccurredAtUtc >= @winStart AND OccurredAtUtc < @winEnd";
|
|
||||||
|
|
||||||
await using (var idsCmd = conn.CreateCommand())
|
await using (var idsCmd = conn.CreateCommand())
|
||||||
{
|
{
|
||||||
idsCmd.CommandText = $@"
|
idsCmd.CommandText = ChainCtePrefix(anchor.EdgesWindowPredicate) + $@"
|
||||||
WITH Edges AS (
|
|
||||||
SELECT DISTINCT ExecutionId, ParentExecutionId
|
|
||||||
FROM dbo.AuditLog
|
|
||||||
WHERE ExecutionId IS NOT NULL
|
|
||||||
{edgesWindowPredicate}
|
|
||||||
),
|
|
||||||
Chain AS (
|
|
||||||
SELECT CAST(@root AS uniqueidentifier) AS ExecutionId,
|
|
||||||
CAST(NULL AS uniqueidentifier) AS ParentExecutionId
|
|
||||||
UNION ALL
|
|
||||||
SELECT e.ExecutionId, e.ParentExecutionId
|
|
||||||
FROM Edges e
|
|
||||||
INNER JOIN Chain c ON e.ParentExecutionId = c.ExecutionId
|
|
||||||
)
|
|
||||||
SELECT ExecutionId FROM Chain
|
SELECT ExecutionId FROM Chain
|
||||||
UNION
|
UNION
|
||||||
SELECT ParentExecutionId FROM Chain WHERE ParentExecutionId IS NOT NULL
|
SELECT ParentExecutionId FROM Chain WHERE ParentExecutionId IS NOT NULL
|
||||||
OPTION (MAXRECURSION {ExecutionChainMaxDepth});";
|
OPTION (MAXRECURSION {ExecutionChainMaxDepth});";
|
||||||
var pRoot = idsCmd.CreateParameter();
|
|
||||||
pRoot.ParameterName = "@root";
|
AddChainParameters(idsCmd, anchor);
|
||||||
pRoot.Value = rootExecutionId;
|
|
||||||
idsCmd.Parameters.Add(pRoot);
|
|
||||||
if (rootFirstOccurred is { } rootFirst)
|
|
||||||
{
|
|
||||||
var pWinStart = idsCmd.CreateParameter();
|
|
||||||
pWinStart.ParameterName = "@winStart";
|
|
||||||
pWinStart.Value = rootFirst - ExecutionTreeWindowSlack;
|
|
||||||
idsCmd.Parameters.Add(pWinStart);
|
|
||||||
var pWinEnd = idsCmd.CreateParameter();
|
|
||||||
pWinEnd.ParameterName = "@winEnd";
|
|
||||||
pWinEnd.Value = rootFirst + ExecutionTreeMaxSpan;
|
|
||||||
idsCmd.Parameters.Add(pWinEnd);
|
|
||||||
}
|
|
||||||
await using var reader = await idsCmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
await using var reader = await idsCmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user