galaxy: add by-name and by-path indexes to GalaxyHierarchyIndex
This commit is contained in:
@@ -80,9 +80,7 @@ public static class GalaxyBrowseProjector
|
||||
return request.ParentGobjectId;
|
||||
case BrowseChildrenRequest.ParentOneofCase.ParentTagName:
|
||||
{
|
||||
GalaxyObjectView? match = entry.Index.ObjectViews.FirstOrDefault(
|
||||
view => string.Equals(view.Object.TagName, request.ParentTagName, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is null)
|
||||
if (!entry.Index.ObjectViewsByTagName.TryGetValue(request.ParentTagName, out GalaxyObjectView? match))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
||||
}
|
||||
@@ -90,9 +88,7 @@ public static class GalaxyBrowseProjector
|
||||
}
|
||||
case BrowseChildrenRequest.ParentOneofCase.ParentContainedPath:
|
||||
{
|
||||
GalaxyObjectView? match = entry.Index.ObjectViews.FirstOrDefault(
|
||||
view => string.Equals(view.ContainedPath, request.ParentContainedPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (match is null)
|
||||
if (!entry.Index.ObjectViewsByContainedPath.TryGetValue(request.ParentContainedPath, out GalaxyObjectView? match))
|
||||
{
|
||||
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
||||
}
|
||||
|
||||
@@ -8,12 +8,16 @@ public sealed class GalaxyHierarchyIndex
|
||||
IReadOnlyList<GalaxyObjectView> objectViews,
|
||||
IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
|
||||
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
|
||||
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent)
|
||||
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent,
|
||||
IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByTagName,
|
||||
IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByContainedPath)
|
||||
{
|
||||
ObjectViews = objectViews;
|
||||
ObjectViewsById = objectViewsById;
|
||||
TagsByAddress = tagsByAddress;
|
||||
ChildrenByParent = childrenByParent;
|
||||
ObjectViewsByTagName = objectViewsByTagName;
|
||||
ObjectViewsByContainedPath = objectViewsByContainedPath;
|
||||
}
|
||||
|
||||
/// <summary>Gets an empty Galaxy hierarchy index.</summary>
|
||||
@@ -21,7 +25,9 @@ public sealed class GalaxyHierarchyIndex
|
||||
Array.Empty<GalaxyObjectView>(),
|
||||
new Dictionary<int, GalaxyObjectView>(),
|
||||
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase),
|
||||
new Dictionary<int, IReadOnlyList<GalaxyObjectView>>());
|
||||
new Dictionary<int, IReadOnlyList<GalaxyObjectView>>(),
|
||||
new Dictionary<string, GalaxyObjectView>(StringComparer.OrdinalIgnoreCase),
|
||||
new Dictionary<string, GalaxyObjectView>(StringComparer.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>Gets the object views.</summary>
|
||||
public IReadOnlyList<GalaxyObjectView> ObjectViews { get; }
|
||||
@@ -35,6 +41,12 @@ public sealed class GalaxyHierarchyIndex
|
||||
/// <summary>Gets direct children grouped by parent gobject id. Root objects (no parent, or self-parented) live under key 0. Each list is sorted areas-first, then by display name (OrdinalIgnoreCase).</summary>
|
||||
public IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> ChildrenByParent { get; }
|
||||
|
||||
/// <summary>Gets object views indexed by <see cref="GalaxyObject.TagName"/> (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by tag name in O(1) instead of scanning <see cref="ObjectViews"/>.</summary>
|
||||
public IReadOnlyDictionary<string, GalaxyObjectView> ObjectViewsByTagName { get; }
|
||||
|
||||
/// <summary>Gets object views indexed by contained path (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by path in O(1) instead of scanning <see cref="ObjectViews"/>.</summary>
|
||||
public IReadOnlyDictionary<string, GalaxyObjectView> ObjectViewsByContainedPath { get; }
|
||||
|
||||
/// <summary>Builds a Galaxy hierarchy index from the given objects.</summary>
|
||||
/// <param name="objects">The Galaxy objects to index.</param>
|
||||
/// <returns>A new Galaxy hierarchy index.</returns>
|
||||
@@ -54,6 +66,8 @@ public sealed class GalaxyHierarchyIndex
|
||||
List<GalaxyObjectView> views = new(objects.Count);
|
||||
Dictionary<int, GalaxyObjectView> viewsById = new();
|
||||
Dictionary<string, GalaxyTagLookup> tagsByAddress = new(StringComparer.OrdinalIgnoreCase);
|
||||
Dictionary<string, GalaxyObjectView> viewsByTagName = new(StringComparer.OrdinalIgnoreCase);
|
||||
Dictionary<string, GalaxyObjectView> viewsByContainedPath = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (GalaxyObject obj in objects)
|
||||
{
|
||||
@@ -66,6 +80,12 @@ public sealed class GalaxyHierarchyIndex
|
||||
if (!string.IsNullOrWhiteSpace(obj.TagName))
|
||||
{
|
||||
tagsByAddress.TryAdd(obj.TagName, new GalaxyTagLookup(obj, Attribute: null, path));
|
||||
viewsByTagName.TryAdd(obj.TagName, view);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
viewsByContainedPath.TryAdd(path, view);
|
||||
}
|
||||
|
||||
foreach (GalaxyAttribute attribute in obj.Attributes)
|
||||
@@ -109,7 +129,9 @@ public sealed class GalaxyHierarchyIndex
|
||||
views,
|
||||
viewsById,
|
||||
tagsByAddress,
|
||||
readOnlyChildren);
|
||||
readOnlyChildren,
|
||||
viewsByTagName,
|
||||
viewsByContainedPath);
|
||||
}
|
||||
|
||||
private static string BuildContainedPath(
|
||||
|
||||
@@ -103,7 +103,7 @@ public static class GalaxyHierarchyProjector
|
||||
// ResolveRoot can throw RpcException(NotFound); run it before consulting the
|
||||
// memo so a bad root surfaces consistently regardless of cache state.
|
||||
IReadOnlyList<GalaxyObjectView> views = entry.Index.ObjectViews;
|
||||
GalaxyObjectView? root = ResolveRoot(request, views);
|
||||
GalaxyObjectView? root = ResolveRoot(request, entry.Index);
|
||||
|
||||
ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>> memo =
|
||||
FilteredViewCache.GetValue(entry, static _ => new ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>>(StringComparer.Ordinal));
|
||||
@@ -176,17 +176,17 @@ public static class GalaxyHierarchyProjector
|
||||
|
||||
private static GalaxyObjectView? ResolveRoot(
|
||||
DiscoverHierarchyRequest request,
|
||||
IReadOnlyList<GalaxyObjectView> views)
|
||||
GalaxyHierarchyIndex index)
|
||||
{
|
||||
GalaxyObjectView? root = request.RootCase switch
|
||||
{
|
||||
DiscoverHierarchyRequest.RootOneofCase.None => null,
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootGobjectId => views.FirstOrDefault(
|
||||
view => view.Object.GobjectId == request.RootGobjectId),
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootTagName => views.FirstOrDefault(
|
||||
view => string.Equals(view.Object.TagName, request.RootTagName, StringComparison.OrdinalIgnoreCase)),
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootContainedPath => views.FirstOrDefault(
|
||||
view => string.Equals(view.ContainedPath, request.RootContainedPath, StringComparison.OrdinalIgnoreCase)),
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootGobjectId =>
|
||||
index.ObjectViewsById.TryGetValue(request.RootGobjectId, out GalaxyObjectView? byId) ? byId : null,
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootTagName =>
|
||||
index.ObjectViewsByTagName.TryGetValue(request.RootTagName, out GalaxyObjectView? byTag) ? byTag : null,
|
||||
DiscoverHierarchyRequest.RootOneofCase.RootContainedPath =>
|
||||
index.ObjectViewsByContainedPath.TryGetValue(request.RootContainedPath, out GalaxyObjectView? byPath) ? byPath : null,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
|
||||
@@ -75,6 +75,47 @@ public sealed class GalaxyHierarchyIndexTests
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies <see cref="GalaxyHierarchyIndex.ObjectViewsByTagName"/> is OrdinalIgnoreCase and supports O(1) lookups.</summary>
|
||||
[Fact]
|
||||
public void ObjectViewsByTagName_IsCaseInsensitive_AndLookupsAreO1()
|
||||
{
|
||||
GalaxyObject root = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "Plant", BrowseName = "Plant", TagName = "Plant" };
|
||||
GalaxyObject mixer = new() { GobjectId = 2, ParentGobjectId = 1, ContainedName = "Mixer_001", BrowseName = "Mixer_001", TagName = "Plant.Mixer_001" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([root, mixer]);
|
||||
|
||||
Assert.True(index.ObjectViewsByTagName.TryGetValue("Plant.Mixer_001", out GalaxyObjectView? exact));
|
||||
Assert.NotNull(exact);
|
||||
Assert.Equal(2, exact!.Object.GobjectId);
|
||||
|
||||
// Case-insensitive lookup must hit the same entry.
|
||||
Assert.True(index.ObjectViewsByTagName.TryGetValue("plant.mixer_001", out GalaxyObjectView? lower));
|
||||
Assert.NotNull(lower);
|
||||
Assert.Same(exact, lower);
|
||||
|
||||
Assert.False(index.ObjectViewsByTagName.ContainsKey("Plant.Missing"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies <see cref="GalaxyHierarchyIndex.ObjectViewsByContainedPath"/> is OrdinalIgnoreCase.</summary>
|
||||
[Fact]
|
||||
public void ObjectViewsByContainedPath_IsCaseInsensitive()
|
||||
{
|
||||
GalaxyObject root = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "Plant", BrowseName = "Plant", TagName = "Plant" };
|
||||
GalaxyObject lineA = new() { GobjectId = 2, ParentGobjectId = 1, IsArea = true, ContainedName = "Line_A", BrowseName = "Line_A", TagName = "Plant.Line_A" };
|
||||
|
||||
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([root, lineA]);
|
||||
|
||||
Assert.True(index.ObjectViewsByContainedPath.TryGetValue("Plant/Line_A", out GalaxyObjectView? exact));
|
||||
Assert.NotNull(exact);
|
||||
Assert.Equal(2, exact!.Object.GobjectId);
|
||||
|
||||
Assert.True(index.ObjectViewsByContainedPath.TryGetValue("plant/line_a", out GalaxyObjectView? lower));
|
||||
Assert.NotNull(lower);
|
||||
Assert.Same(exact, lower);
|
||||
|
||||
Assert.False(index.ObjectViewsByContainedPath.ContainsKey("Plant/Missing"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies children sort areas-first, then by display name (case-insensitive).</summary>
|
||||
[Fact]
|
||||
public void ChildrenByParent_SortsAreasFirstThenByDisplayName()
|
||||
|
||||
Reference in New Issue
Block a user