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;
|
return request.ParentGobjectId;
|
||||||
case BrowseChildrenRequest.ParentOneofCase.ParentTagName:
|
case BrowseChildrenRequest.ParentOneofCase.ParentTagName:
|
||||||
{
|
{
|
||||||
GalaxyObjectView? match = entry.Index.ObjectViews.FirstOrDefault(
|
if (!entry.Index.ObjectViewsByTagName.TryGetValue(request.ParentTagName, out GalaxyObjectView? match))
|
||||||
view => string.Equals(view.Object.TagName, request.ParentTagName, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (match is null)
|
|
||||||
{
|
{
|
||||||
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
||||||
}
|
}
|
||||||
@@ -90,9 +88,7 @@ public static class GalaxyBrowseProjector
|
|||||||
}
|
}
|
||||||
case BrowseChildrenRequest.ParentOneofCase.ParentContainedPath:
|
case BrowseChildrenRequest.ParentOneofCase.ParentContainedPath:
|
||||||
{
|
{
|
||||||
GalaxyObjectView? match = entry.Index.ObjectViews.FirstOrDefault(
|
if (!entry.Index.ObjectViewsByContainedPath.TryGetValue(request.ParentContainedPath, out GalaxyObjectView? match))
|
||||||
view => string.Equals(view.ContainedPath, request.ParentContainedPath, StringComparison.OrdinalIgnoreCase));
|
|
||||||
if (match is null)
|
|
||||||
{
|
{
|
||||||
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
throw new RpcException(new Status(StatusCode.NotFound, "BrowseChildren parent was not found."));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,12 +8,16 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
IReadOnlyList<GalaxyObjectView> objectViews,
|
IReadOnlyList<GalaxyObjectView> objectViews,
|
||||||
IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
|
IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
|
||||||
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
|
IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
|
||||||
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent)
|
IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent,
|
||||||
|
IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByTagName,
|
||||||
|
IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByContainedPath)
|
||||||
{
|
{
|
||||||
ObjectViews = objectViews;
|
ObjectViews = objectViews;
|
||||||
ObjectViewsById = objectViewsById;
|
ObjectViewsById = objectViewsById;
|
||||||
TagsByAddress = tagsByAddress;
|
TagsByAddress = tagsByAddress;
|
||||||
ChildrenByParent = childrenByParent;
|
ChildrenByParent = childrenByParent;
|
||||||
|
ObjectViewsByTagName = objectViewsByTagName;
|
||||||
|
ObjectViewsByContainedPath = objectViewsByContainedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Gets an empty Galaxy hierarchy index.</summary>
|
/// <summary>Gets an empty Galaxy hierarchy index.</summary>
|
||||||
@@ -21,7 +25,9 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
Array.Empty<GalaxyObjectView>(),
|
Array.Empty<GalaxyObjectView>(),
|
||||||
new Dictionary<int, GalaxyObjectView>(),
|
new Dictionary<int, GalaxyObjectView>(),
|
||||||
new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase),
|
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>
|
/// <summary>Gets the object views.</summary>
|
||||||
public IReadOnlyList<GalaxyObjectView> ObjectViews { get; }
|
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>
|
/// <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; }
|
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>
|
/// <summary>Builds a Galaxy hierarchy index from the given objects.</summary>
|
||||||
/// <param name="objects">The Galaxy objects to index.</param>
|
/// <param name="objects">The Galaxy objects to index.</param>
|
||||||
/// <returns>A new Galaxy hierarchy index.</returns>
|
/// <returns>A new Galaxy hierarchy index.</returns>
|
||||||
@@ -54,6 +66,8 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
List<GalaxyObjectView> views = new(objects.Count);
|
List<GalaxyObjectView> views = new(objects.Count);
|
||||||
Dictionary<int, GalaxyObjectView> viewsById = new();
|
Dictionary<int, GalaxyObjectView> viewsById = new();
|
||||||
Dictionary<string, GalaxyTagLookup> tagsByAddress = new(StringComparer.OrdinalIgnoreCase);
|
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)
|
foreach (GalaxyObject obj in objects)
|
||||||
{
|
{
|
||||||
@@ -66,6 +80,12 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
if (!string.IsNullOrWhiteSpace(obj.TagName))
|
if (!string.IsNullOrWhiteSpace(obj.TagName))
|
||||||
{
|
{
|
||||||
tagsByAddress.TryAdd(obj.TagName, new GalaxyTagLookup(obj, Attribute: null, path));
|
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)
|
foreach (GalaxyAttribute attribute in obj.Attributes)
|
||||||
@@ -109,7 +129,9 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
views,
|
views,
|
||||||
viewsById,
|
viewsById,
|
||||||
tagsByAddress,
|
tagsByAddress,
|
||||||
readOnlyChildren);
|
readOnlyChildren,
|
||||||
|
viewsByTagName,
|
||||||
|
viewsByContainedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildContainedPath(
|
private static string BuildContainedPath(
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public static class GalaxyHierarchyProjector
|
|||||||
// ResolveRoot can throw RpcException(NotFound); run it before consulting the
|
// ResolveRoot can throw RpcException(NotFound); run it before consulting the
|
||||||
// memo so a bad root surfaces consistently regardless of cache state.
|
// memo so a bad root surfaces consistently regardless of cache state.
|
||||||
IReadOnlyList<GalaxyObjectView> views = entry.Index.ObjectViews;
|
IReadOnlyList<GalaxyObjectView> views = entry.Index.ObjectViews;
|
||||||
GalaxyObjectView? root = ResolveRoot(request, views);
|
GalaxyObjectView? root = ResolveRoot(request, entry.Index);
|
||||||
|
|
||||||
ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>> memo =
|
ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>> memo =
|
||||||
FilteredViewCache.GetValue(entry, static _ => new ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>>(StringComparer.Ordinal));
|
FilteredViewCache.GetValue(entry, static _ => new ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>>(StringComparer.Ordinal));
|
||||||
@@ -176,17 +176,17 @@ public static class GalaxyHierarchyProjector
|
|||||||
|
|
||||||
private static GalaxyObjectView? ResolveRoot(
|
private static GalaxyObjectView? ResolveRoot(
|
||||||
DiscoverHierarchyRequest request,
|
DiscoverHierarchyRequest request,
|
||||||
IReadOnlyList<GalaxyObjectView> views)
|
GalaxyHierarchyIndex index)
|
||||||
{
|
{
|
||||||
GalaxyObjectView? root = request.RootCase switch
|
GalaxyObjectView? root = request.RootCase switch
|
||||||
{
|
{
|
||||||
DiscoverHierarchyRequest.RootOneofCase.None => null,
|
DiscoverHierarchyRequest.RootOneofCase.None => null,
|
||||||
DiscoverHierarchyRequest.RootOneofCase.RootGobjectId => views.FirstOrDefault(
|
DiscoverHierarchyRequest.RootOneofCase.RootGobjectId =>
|
||||||
view => view.Object.GobjectId == request.RootGobjectId),
|
index.ObjectViewsById.TryGetValue(request.RootGobjectId, out GalaxyObjectView? byId) ? byId : null,
|
||||||
DiscoverHierarchyRequest.RootOneofCase.RootTagName => views.FirstOrDefault(
|
DiscoverHierarchyRequest.RootOneofCase.RootTagName =>
|
||||||
view => string.Equals(view.Object.TagName, request.RootTagName, StringComparison.OrdinalIgnoreCase)),
|
index.ObjectViewsByTagName.TryGetValue(request.RootTagName, out GalaxyObjectView? byTag) ? byTag : null,
|
||||||
DiscoverHierarchyRequest.RootOneofCase.RootContainedPath => views.FirstOrDefault(
|
DiscoverHierarchyRequest.RootOneofCase.RootContainedPath =>
|
||||||
view => string.Equals(view.ContainedPath, request.RootContainedPath, StringComparison.OrdinalIgnoreCase)),
|
index.ObjectViewsByContainedPath.TryGetValue(request.RootContainedPath, out GalaxyObjectView? byPath) ? byPath : null,
|
||||||
_ => 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>
|
/// <summary>Verifies children sort areas-first, then by display name (case-insensitive).</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ChildrenByParent_SortsAreasFirstThenByDisplayName()
|
public void ChildrenByParent_SortsAreasFirstThenByDisplayName()
|
||||||
|
|||||||
Reference in New Issue
Block a user