< 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: 63
Uncovered lines: 0
Coverable lines: 63
Total lines: 200
Line coverage: 100%
Branch coverage
100%
Covered branches: 44
Total branches: 44
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%44100%
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{
 888611    private readonly Dictionary<string, string> _mappings = new();
 12
 13    /// <summary>
 14    /// Sets a single query-name → field-key mapping.
 15    /// </summary>
 33316    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>
 30623    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    {
 1630832        if (definition.Fields.Count > 0)
 33        {
 34            // Keep existing root mapping if it exists and points to a valid field
 1628135            if (_mappings.TryGetValue(definition.Name, out var existingPath) &&
 1628136                definition.Fields.ContainsKey(existingPath))
 37            {
 750038                return; // Keep the existing valid mapping
 39            }
 40
 41            // If we don't have a valid mapping, use the first field. Iterate via the struct
 42            // enumerator (Dictionary.Enumerator is a struct on the concrete Dictionary type) instead
 43            // of LINQ First() which allocates an interface enumerator.
 878144            using var enumerator = definition.Fields.GetEnumerator();
 878145            if (enumerator.MoveNext())
 46            {
 878147                _mappings[definition.Name] = enumerator.Current.Value._effectiveName;
 48            }
 49        }
 880850    }
 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    {
 30667        var rootPath = GetMappedPath(queryName);
 30968        if (string.IsNullOrEmpty(rootPath)) return [];
 46569        if (string.IsNullOrEmpty(nodePath)) return [rootPath];
 70
 14171        if (TryGetCachedPath(pathIndex, rootPath, nodePath, out var cached, out var perRoot))
 72        {
 673            return cached;
 74        }
 75
 13576        var rootField = FindRootField(queryDefinition, rootPath);
 13577        var computedPath = rootField is null ? [rootPath] : BuildPathToNode(rootField, nodePath);
 78
 13579        CachePath(pathIndex, rootPath, nodePath, computedPath, perRoot);
 13580        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.
 14193        cached = [];
 14194        perRoot = null;
 27095        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).
 135109        if (perRoot is null)
 110        {
 129111            perRoot = new Dictionary<string, string[]>(StringComparer.Ordinal);
 129112            pathIndex![rootPath] = perRoot;
 113        }
 135114        perRoot[nodePath] = computedPath;
 135115    }
 116
 117    private static FieldDefinition? FindRootField(QueryDefinition queryDefinition, string rootPath)
 118    {
 135119        if (queryDefinition.Fields.TryGetValue(rootPath, out var field))
 120        {
 63121            return field;
 122        }
 123
 72124        return queryDefinition.Fields.Values.FirstOrDefault(f =>
 144125            string.Equals(f.Alias, rootPath, StringComparison.OrdinalIgnoreCase) ||
 144126            string.Equals(f.Name, rootPath, StringComparison.OrdinalIgnoreCase));
 127    }
 128
 129    private static string[] BuildPathToNode(FieldDefinition rootField, string nodePath)
 130    {
 132131        var targetNode = GetTargetNodeName(nodePath);
 132
 133        // Use List for efficient path building without allocations on each recursion
 132134        var pathBuilder = new List<string>(capacity: 8) { rootField._effectiveName };
 135
 132136        if (FindPathToNodeOptimized(rootField, targetNode, pathBuilder))
 137        {
 138            // Remove the target node itself, keep only the path to it
 126139            if (pathBuilder.Count > 1)
 140            {
 126141                pathBuilder.RemoveAt(pathBuilder.Count - 1);
 142            }
 126143            return pathBuilder.ToArray();
 144        }
 145
 6146        return [rootField._effectiveName];
 147    }
 148
 149    private static ReadOnlySpan<char> GetTargetNodeName(string nodePath)
 150    {
 132151        var nodePathSpan = nodePath.AsSpan();
 132152        var lastDotIndex = nodePathSpan.LastIndexOf('.');
 153
 132154        return lastDotIndex == -1
 132155            ? nodePathSpan
 132156            : nodePathSpan[(lastDotIndex + 1)..];
 157    }
 158
 159    /// <summary>
 160    /// Recursively searches for a target node and builds the path using a shared List.
 161    /// Uses backtracking to avoid allocating new arrays on each recursive call.
 162    /// </summary>
 163    /// <param name="field">The field to search within</param>
 164    /// <param name="targetNode">The target node name to find</param>
 165    /// <param name="pathBuilder">Shared list for building the path (modified in-place)</param>
 166    /// <returns>True if a target node was found, false otherwise</returns>
 167    private static bool FindPathToNodeOptimized(FieldDefinition field, ReadOnlySpan<char> targetNode, List<string> pathB
 168    {
 169        // Iterate _children directly via the lock-free zero-alloc AsSpan() — going through Fields.Values
 170        // would allocate a List wrapper on every recursion via the IReadOnlyDictionary.Values implementation.
 273171        var children = field._children;
 282172        if (children == null) return false;
 173
 264174        var span = children.AsSpan();
 558175        for (int i = 0; i < span.Length; i++)
 176        {
 267177            var childField = span[i];
 267178            var effectiveName = childField._effectiveName;
 179
 267180            var isMatch = targetNode.Equals(effectiveName.AsSpan(), StringComparison.OrdinalIgnoreCase) ||
 267181                          (!ReferenceEquals(effectiveName, childField.Name) && targetNode.Equals(childField.Name.AsSpan(
 182
 267183            pathBuilder.Add(effectiveName);
 267184            if (isMatch)
 185            {
 126186                return true;
 187            }
 188
 141189            if (FindPathToNodeOptimized(childField, targetNode, pathBuilder))
 190            {
 126191                return true;
 192            }
 193
 194            // Backtrack: remove this field from the path since the target wasn't found here.
 15195            pathBuilder.RemoveAt(pathBuilder.Count - 1);
 196        }
 197
 12198        return false;
 199    }
 200}