fix(galaxy): include undeployed areas in browse + re-root orphaned objects
The hierarchy query returned deployed objects only (deployed_package_id <> 0), so areas whose containing area is undeployed were orphaned and hidden from /browse — on wonder, only the lone deployed root area surfaced. Include category-13 Area objects regardless of deployment, and in GalaxyHierarchyIndex re-root any object whose parent is absent from the set (e.g. a deleted container area) so nothing disappears under a phantom parent id.
This commit is contained in:
@@ -106,6 +106,13 @@ public sealed class GalaxyHierarchyIndex
|
|||||||
{
|
{
|
||||||
parentKey = 0;
|
parentKey = 0;
|
||||||
}
|
}
|
||||||
|
// Re-root orphans whose parent object is absent from the set (e.g. a deleted or
|
||||||
|
// never-loaded container area). Otherwise they bucket under a phantom parent id
|
||||||
|
// that is never reached from the root, so they vanish from browse entirely.
|
||||||
|
else if (parentKey != 0 && !objectsById.ContainsKey(parentKey))
|
||||||
|
{
|
||||||
|
parentKey = 0;
|
||||||
|
}
|
||||||
if (!childrenByParent.TryGetValue(parentKey, out List<GalaxyObjectView>? bucket))
|
if (!childrenByParent.TryGetValue(parentKey, out List<GalaxyObjectView>? bucket))
|
||||||
{
|
{
|
||||||
bucket = [];
|
bucket = [];
|
||||||
|
|||||||
@@ -172,6 +172,11 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
|
|||||||
AckCommentSubtag = string.Empty,
|
AckCommentSubtag = string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Area objects (category 13) are returned even when undeployed (deployed_package_id = 0):
|
||||||
|
// they are organizational/model nodes that group deployed objects, so excluding them
|
||||||
|
// orphans every area whose containing area is not itself deployed. All non-area objects
|
||||||
|
// still require deployment. Orphans left by a missing/deleted parent area are re-rooted
|
||||||
|
// by GalaxyHierarchyIndex.Build so nothing disappears from browse.
|
||||||
private const string HierarchySql = @"
|
private const string HierarchySql = @"
|
||||||
;WITH template_chain AS (
|
;WITH template_chain AS (
|
||||||
SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id,
|
SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id,
|
||||||
@@ -218,7 +223,7 @@ INNER JOIN template_definition td
|
|||||||
ON g.template_definition_id = td.template_definition_id
|
ON g.template_definition_id = td.template_definition_id
|
||||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||||
AND g.is_template = 0
|
AND g.is_template = 0
|
||||||
AND g.deployed_package_id <> 0
|
AND (g.deployed_package_id <> 0 OR td.category_id = 13)
|
||||||
ORDER BY parent_gobject_id, g.tag_name";
|
ORDER BY parent_gobject_id, g.tag_name";
|
||||||
|
|
||||||
// Unlike HierarchySql, this query has diverged from the OtOpcUa original. It returns two
|
// Unlike HierarchySql, this query has diverged from the OtOpcUa original. It returns two
|
||||||
|
|||||||
@@ -75,6 +75,35 @@ public sealed class GalaxyHierarchyIndexTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Verifies an object whose parent is absent from the set (e.g. a deleted/undeployed
|
||||||
|
/// container area) is re-rooted under sentinel 0 rather than vanishing under a phantom
|
||||||
|
/// parent id that browse never reaches from the root.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ChildrenByParent_OrphanWithMissingParent_AppearsAsRoot()
|
||||||
|
{
|
||||||
|
GalaxyObject realRoot = new() { GobjectId = 1, ParentGobjectId = 0, IsArea = true, ContainedName = "RealRoot" };
|
||||||
|
GalaxyObject orphanArea = new() { GobjectId = 2, ParentGobjectId = 5008, IsArea = true, ContainedName = "Orphan" };
|
||||||
|
GalaxyObject orphanChild = new() { GobjectId = 3, ParentGobjectId = 2, ContainedName = "Child" };
|
||||||
|
|
||||||
|
GalaxyHierarchyIndex index = GalaxyHierarchyIndex.Build([realRoot, orphanArea, orphanChild]);
|
||||||
|
|
||||||
|
// Both the real root and the orphan (its parent 5008 is absent) surface under root.
|
||||||
|
Assert.True(index.ChildrenByParent.TryGetValue(0, out IReadOnlyList<GalaxyObjectView>? roots));
|
||||||
|
Assert.NotNull(roots);
|
||||||
|
Assert.Contains(roots!, view => view.Object.GobjectId == 1);
|
||||||
|
Assert.Contains(roots!, view => view.Object.GobjectId == 2);
|
||||||
|
|
||||||
|
// The orphan keeps its own deployed children nested beneath it.
|
||||||
|
Assert.True(index.ChildrenByParent.TryGetValue(2, out IReadOnlyList<GalaxyObjectView>? underOrphan));
|
||||||
|
Assert.Single(underOrphan!);
|
||||||
|
Assert.Equal(3, underOrphan![0].Object.GobjectId);
|
||||||
|
|
||||||
|
// Nothing buckets under the phantom parent id.
|
||||||
|
Assert.False(index.ChildrenByParent.ContainsKey(5008));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies <see cref="GalaxyHierarchyIndex.ObjectViewsByTagName"/> is OrdinalIgnoreCase and supports O(1) lookups.</summary>
|
/// <summary>Verifies <see cref="GalaxyHierarchyIndex.ObjectViewsByTagName"/> is OrdinalIgnoreCase and supports O(1) lookups.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void ObjectViewsByTagName_IsCaseInsensitive_AndLookupsAreO1()
|
public void ObjectViewsByTagName_IsCaseInsensitive_AndLookupsAreO1()
|
||||||
|
|||||||
Reference in New Issue
Block a user