< Summary

Information
Class: NGql.Core.Features.QueryMap
Assembly: NGql.Core
File(s): /home/runner/work/NGql/NGql/src/Core/Features/QueryMap.cs
Line coverage
100%
Covered lines: 66
Uncovered lines: 0
Coverable lines: 66
Total lines: 210
Line coverage: 100%
Branch coverage
100%
Covered branches: 48
Total branches: 48
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor()100%11100%
SetMapping(...)100%11100%
GetMappedPath(...)100%11100%
UpdateRootMapping(...)100%88100%
GetPathTo(...)100%88100%
TryGetCachedPath(...)100%44100%
CachePath(...)100%22100%
FindRootField(...)100%88100%
BuildPathToNode(...)100%44100%
GetTargetNodeName(...)100%22100%
FindPathToNodeOptimized(...)100%1212100%

File(s)

/home/runner/work/NGql/NGql/src/Core/Features/QueryMap.cs

#LineLine coverage
 1using NGql.Core.Abstractions;
 2
 3namespace NGql.Core.Features;
 4
 5/// <summary>
 6/// Manages query name mappings and provides path resolution functionality.
 7/// Maps original query names to their merged definition names and handles path navigation.
 8/// </summary>
 9internal sealed class QueryMap
 10{
 1208711    private readonly Dictionary<string, string> _mappings = new();
 12
 13    /// <summary>
 14    /// Sets a single query-name → field-key mapping.
 15    /// </summary>
 216016    public void SetMapping(string queryName, string fieldKey) => _mappings[queryName] = fieldKey;
 17
 18    /// <summary>
 19    /// Gets the mapped path for a query name or returns the original name if no mapping exists.
 20    /// </summary>
 21    /// <param name="queryName">The query name to look up</param>
 22    /// <returns>The mapped path or the original query name</returns>
 35123    private string GetMappedPath(string queryName) => _mappings.GetValueOrDefault(queryName, queryName);
 24
 25    /// <summary>
 26    /// Updates the QueryMap entry for the root query to point to its first field's alias/name.
 27    /// This is typically called after field modifications to maintain accurate mappings.
 28    /// </summary>
 29    /// <param name="definition">The query definition to update mapping for</param>
 30    public void UpdateRootMapping(QueryDefinition definition)
 31    {
 3726032        if (definition.Fields.Count > 0)
 33        {
 34            // Keep existing root mapping if it exists and points to a valid field
 3722735            if (_mappings.TryGetValue(definition.Name, out var existingPath) &&
 3722736                definition.Fields.ContainsKey(existingPath))
 37            {
 2526338                return; // Keep the existing valid mapping
 39            }
 40
 41            // If we don't have a valid mapping, use the first field. Iterate the concrete
 42            // dictionary via its struct enumerator; going through the read-only interface would
 43            // box the enumerator and allocate on every build.
 1196444            using var enumerator = definition.FieldsInternal.GetEnumerator();
 1196445            if (enumerator.MoveNext())
 46            {
 1196447                _mappings[definition.Name] = enumerator.Current.Value._effectiveName;
 48            }
 49        }
 1199750    }
 51
 52    /// <summary>
 53    /// Gets the path segments to reach a specific node within a query.
 54    /// Uses the path index for O(1) lookup when available, falls back to DFS traversal.
 55    /// </summary>
 56    /// <param name="queryName">The name of the query to find the path for.</param>
 57    /// <param name="nodePath">The optional node path within the query (e.g., "edges.node").</param>
 58    /// <param name="queryDefinition">The query definition to search within</param>
 59    /// <param name="pathIndex">Optional path index for O(1) lookups (performance optimization)</param>
 60    /// <returns>An array of path segments to reach the specified node.</returns>
 61    internal string[] GetPathTo(
 62        string queryName,
 63        string? nodePath,
 64        QueryDefinition queryDefinition,
 65        Dictionary<string, Dictionary<string, string[]>>? pathIndex = null)
 66    {
 35167        var rootPath = GetMappedPath(queryName);
 35468        if (string.IsNullOrEmpty(rootPath)) return [];
 51969        if (string.IsNullOrEmpty(nodePath)) return [rootPath];
 70
 17771        if (TryGetCachedPath(pathIndex, rootPath, nodePath, out var cached, out var perRoot))
 72        {
 673            return cached;
 74        }
 75
 17176        var rootField = FindRootField(queryDefinition, rootPath);
 17177        var computedPath = rootField is null ? [rootPath] : BuildPathToNode(rootField, nodePath);
 78
 17179        CachePath(pathIndex, rootPath, nodePath, computedPath, perRoot);
 17180        return computedPath;
 81    }
 82
 83    private static bool TryGetCachedPath(
 84        Dictionary<string, Dictionary<string, string[]>>? pathIndex,
 85        string rootPath,
 86        string nodePath,
 87        out string[] cached,
 88        out Dictionary<string, string[]>? perRoot)
 89    {
 90        // QueryBuilder always passes a non-null _pathIndex; the parameter type allows null
 91        // for the GetPathTo overload that takes no path-index, but the caller in this file
 92        // never invokes TryGetCachedPath unless pathIndex is non-null in the GetPathTo wrapper.
 17793        cached = [];
 17794        perRoot = null;
 34295        if (!pathIndex!.TryGetValue(rootPath, out perRoot)) return false;
 1896        if (!perRoot.TryGetValue(nodePath, out var hit)) return false;
 697        cached = hit;
 698        return true;
 99    }
 100
 101    private static void CachePath(
 102        Dictionary<string, Dictionary<string, string[]>>? pathIndex,
 103        string rootPath,
 104        string nodePath,
 105        string[] computedPath,
 106        Dictionary<string, string[]>? perRoot)
 107    {
 108        // QueryBuilder always passes a non-null pathIndex (see TryGetCachedPath comment).
 171109        if (perRoot is null)
 110        {
 165111            perRoot = new Dictionary<string, string[]>(StringComparer.Ordinal);
 165112            pathIndex![rootPath] = perRoot;
 113        }
 171114        perRoot[nodePath] = computedPath;
 171115    }
 116
 117    [System.Diagnostics.CodeAnalysis.SuppressMessage(
 118        "Major Code Smell", "S3267:Loops should be simplified using the \"Where\" LINQ method",
 119        Justification = "Runs on every GetPathTo cache miss — a plain foreach over Dictionary.Values uses the struct enu
 120    private static FieldDefinition? FindRootField(QueryDefinition queryDefinition, string rootPath)
 121    {
 171122        if (queryDefinition.FieldsInternal.TryGetValue(rootPath, out var field))
 123        {
 84124            return field;
 125        }
 126
 264127        foreach (var candidate in queryDefinition.FieldsInternal.Values)
 128        {
 87129            if (string.Equals(candidate.Alias, rootPath, StringComparison.OrdinalIgnoreCase) ||
 87130                string.Equals(candidate.Name, rootPath, StringComparison.OrdinalIgnoreCase))
 131            {
 84132                return candidate;
 133            }
 134        }
 135
 3136        return null;
 84137    }
 138
 139    private static string[] BuildPathToNode(FieldDefinition rootField, string nodePath)
 140    {
 168141        var targetNode = GetTargetNodeName(nodePath);
 142
 143        // Use List for efficient path building without allocations on each recursion
 168144        var pathBuilder = new List<string>(capacity: 8) { rootField._effectiveName };
 145
 168146        if (FindPathToNodeOptimized(rootField, targetNode, pathBuilder))
 147        {
 148            // Remove the target node itself, keep only the path to it
 150149            if (pathBuilder.Count > 1)
 150            {
 150151                pathBuilder.RemoveAt(pathBuilder.Count - 1);
 152            }
 150153            return pathBuilder.ToArray();
 154        }
 155
 18156        return [rootField._effectiveName];
 157    }
 158
 159    private static ReadOnlySpan<char> GetTargetNodeName(string nodePath)
 160    {
 168161        var nodePathSpan = nodePath.AsSpan();
 168162        var lastDotIndex = nodePathSpan.LastIndexOf('.');
 163
 168164        return lastDotIndex == -1
 168165            ? nodePathSpan
 168166            : nodePathSpan[(lastDotIndex + 1)..];
 167    }
 168
 169    /// <summary>
 170    /// Recursively searches for a target node and builds the path using a shared List.
 171    /// Uses backtracking to avoid allocating new arrays on each recursive call.
 172    /// </summary>
 173    /// <param name="field">The field to search within</param>
 174    /// <param name="targetNode">The target node name to find</param>
 175    /// <param name="pathBuilder">Shared list for building the path (modified in-place)</param>
 176    /// <returns>True if a target node was found, false otherwise</returns>
 177    private static bool FindPathToNodeOptimized(FieldDefinition field, ReadOnlySpan<char> targetNode, List<string> pathB
 178    {
 179        // Iterate _children directly via the lock-free zero-alloc AsSpan() — going through Fields.Values
 180        // would allocate a List wrapper on every recursion via the IReadOnlyDictionary.Values implementation.
 360181        var children = field._children;
 390182        if (children == null) return false;
 183
 330184        var span = children.AsSpan();
 762185        for (int i = 0; i < span.Length; i++)
 186        {
 342187            var childField = span[i];
 342188            var effectiveName = childField._effectiveName;
 189
 342190            var isMatch = targetNode.Equals(effectiveName.AsSpan(), StringComparison.OrdinalIgnoreCase) ||
 342191                          (!ReferenceEquals(effectiveName, childField.Name) && targetNode.Equals(childField.Name.AsSpan(
 192
 342193            pathBuilder.Add(effectiveName);
 342194            if (isMatch)
 195            {
 150196                return true;
 197            }
 198
 192199            if (FindPathToNodeOptimized(childField, targetNode, pathBuilder))
 200            {
 141201                return true;
 202            }
 203
 204            // Backtrack: remove this field from the path since the target wasn't found here.
 51205            pathBuilder.RemoveAt(pathBuilder.Count - 1);
 206        }
 207
 39208        return false;
 209    }
 210}