server(galaxy): GetAlarmAttributesAsync discovery query + alarm-attribute row mapping (Task 11)
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
namespace ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
/// <summary>
|
||||
/// One alarm-bearing attribute discovered by
|
||||
/// <see cref="GalaxyRepository.GetAlarmAttributesAsync"/>: an attribute whose owning
|
||||
/// object configures an <c>AlarmExtension</c> primitive (the same
|
||||
/// <c>is_alarm</c> detection used by <see cref="GalaxyRepository.GetAttributesAsync"/>).
|
||||
/// Used to build the subtag-fallback watch-list.
|
||||
/// </summary>
|
||||
public sealed class GalaxyAlarmAttributeRow
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the alarm-bearing attribute reference (e.g. <c>Tank01.Level.HiHi</c>),
|
||||
/// matching the <c>full_tag_reference</c> projection of
|
||||
/// <see cref="GalaxyRepository.GetAttributesAsync"/>.
|
||||
/// </summary>
|
||||
public string FullTagReference { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the owning object reference (e.g. <c>Tank01</c>). This is the Galaxy
|
||||
/// <c>tag_name</c> — the segment that precedes the first attribute dot in
|
||||
/// <see cref="FullTagReference"/>.
|
||||
/// </summary>
|
||||
public string SourceObjectReference { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the writable ack-comment attribute address.
|
||||
/// <para>
|
||||
/// The Galaxy Repository schema does not expose an ack-comment subtag address
|
||||
/// directly, so this is always <see cref="string.Empty"/> here. The watch-list
|
||||
/// resolver (a later task) composes the concrete address from configuration plus
|
||||
/// <see cref="SourceObjectReference"/> / <see cref="FullTagReference"/>.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public string AckCommentSubtag { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -114,6 +114,56 @@ public sealed class GalaxyRepository(GalaxyRepositoryOptions options) : IGalaxyR
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves only the alarm-bearing attributes for the subtag-fallback watch-list.
|
||||
/// Alarm detection is identical to <see cref="GetAttributesAsync"/>: a row is
|
||||
/// alarm-bearing when its owning object configures an <c>AlarmExtension</c>
|
||||
/// primitive (the same <c>is_alarm</c> projection, here applied as a SQL filter).
|
||||
/// </summary>
|
||||
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||
public async Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
{
|
||||
List<GalaxyAlarmAttributeRow> rows = new();
|
||||
|
||||
using SqlConnection conn = new(options.ConnectionString);
|
||||
await conn.OpenAsync(ct).ConfigureAwait(false);
|
||||
|
||||
using SqlCommand cmd = new(AlarmAttributesSql, conn) { CommandTimeout = options.CommandTimeoutSeconds };
|
||||
using SqlDataReader reader = await cmd.ExecuteReaderAsync(ct).ConfigureAwait(false);
|
||||
|
||||
while (await reader.ReadAsync(ct).ConfigureAwait(false))
|
||||
{
|
||||
rows.Add(MapAlarmRow(
|
||||
fullTagReference: reader.GetString(0),
|
||||
sourceObjectReference: reader.GetString(1)));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Maps a raw alarm-attribute reader row to a <see cref="GalaxyAlarmAttributeRow"/>.
|
||||
/// <para>
|
||||
/// <paramref name="sourceObjectReference"/> is the Galaxy <c>tag_name</c> (the
|
||||
/// owning object), and <paramref name="fullTagReference"/> is
|
||||
/// <c>tag_name + '.' + attribute_name</c> — the same composition the
|
||||
/// <c>full_tag_reference</c> projection of <see cref="AttributesSql"/> produces.
|
||||
/// <see cref="GalaxyAlarmAttributeRow.AckCommentSubtag"/> is left empty here; the
|
||||
/// schema does not expose an ack-comment address and the watch-list resolver
|
||||
/// composes it later.
|
||||
/// </para>
|
||||
/// Exposed internally so the derivation can be unit-tested without a database.
|
||||
/// </summary>
|
||||
/// <param name="fullTagReference">The alarm-bearing attribute reference.</param>
|
||||
/// <param name="sourceObjectReference">The owning object reference (tag name).</param>
|
||||
internal static GalaxyAlarmAttributeRow MapAlarmRow(
|
||||
string fullTagReference,
|
||||
string sourceObjectReference) => new()
|
||||
{
|
||||
FullTagReference = fullTagReference,
|
||||
SourceObjectReference = sourceObjectReference,
|
||||
AckCommentSubtag = string.Empty,
|
||||
};
|
||||
|
||||
private const string HierarchySql = @"
|
||||
;WITH template_chain AS (
|
||||
SELECT g.gobject_id AS instance_gobject_id, t.gobject_id AS template_gobject_id,
|
||||
@@ -248,5 +298,71 @@ SELECT
|
||||
FROM ranked r
|
||||
LEFT JOIN data_type dt ON dt.mx_data_type = r.mx_data_type
|
||||
WHERE r.rn = 1
|
||||
ORDER BY r.tag_name, r.attribute_name";
|
||||
|
||||
// Alarm-only discovery for the subtag-fallback watch-list. This deliberately reuses the
|
||||
// exact candidate/ranked CTE structure and the same `AlarmExtension`-based is_alarm
|
||||
// detection as AttributesSql so the two queries cannot drift: a row qualifies only when
|
||||
// its user attribute (src_pri 0) anchors an `AlarmExtension` primitive on the owning
|
||||
// object. It projects just what the watch-list needs — full_tag_reference (tag_name +
|
||||
// '.' + attribute_name, matching AttributesSql) and the owning object's tag_name as
|
||||
// source_object_reference. The array `[]` suffix is intentionally omitted: an
|
||||
// alarm-bearing attribute is a scalar anchor, not an array body.
|
||||
private const string AlarmAttributesSql = @"
|
||||
;WITH deployed_package_chain AS (
|
||||
SELECT g.gobject_id, p.package_id, p.derived_from_package_id, 0 AS depth
|
||||
FROM gobject g
|
||||
INNER JOIN package p ON p.package_id = g.deployed_package_id
|
||||
WHERE g.is_template = 0 AND g.deployed_package_id <> 0
|
||||
UNION ALL
|
||||
SELECT dpc.gobject_id, p.package_id, p.derived_from_package_id, dpc.depth + 1
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN package p ON p.package_id = dpc.derived_from_package_id
|
||||
WHERE dpc.derived_from_package_id <> 0 AND dpc.depth < 10
|
||||
),
|
||||
candidate AS (
|
||||
SELECT
|
||||
dpc.gobject_id, g.tag_name, da.attribute_name, dpc.depth, 0 AS src_pri
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN dynamic_attribute da ON da.package_id = dpc.package_id
|
||||
INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
|
||||
INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND da.attribute_name NOT LIKE '[_]%'
|
||||
AND da.attribute_name NOT LIKE '%.Description'
|
||||
AND da.mx_attribute_category IN (2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 24)
|
||||
UNION ALL
|
||||
SELECT
|
||||
dpc.gobject_id, g.tag_name,
|
||||
CASE WHEN pi.primitive_name IS NULL OR pi.primitive_name = ''
|
||||
THEN ad.attribute_name
|
||||
ELSE pi.primitive_name + '.' + ad.attribute_name END AS attribute_name,
|
||||
dpc.depth, 1 AS src_pri
|
||||
FROM deployed_package_chain dpc
|
||||
INNER JOIN primitive_instance pi ON pi.package_id = dpc.package_id
|
||||
INNER JOIN attribute_definition ad ON ad.primitive_definition_id = pi.primitive_definition_id
|
||||
INNER JOIN gobject g ON g.gobject_id = dpc.gobject_id
|
||||
INNER JOIN template_definition td ON td.template_definition_id = g.template_definition_id
|
||||
WHERE td.category_id IN (1, 3, 4, 10, 11, 13, 17, 24, 26)
|
||||
AND ad.attribute_name NOT LIKE '[_]%'
|
||||
AND ad.attribute_name NOT LIKE '%.Description'
|
||||
),
|
||||
ranked AS (
|
||||
SELECT c.*, ROW_NUMBER() OVER (
|
||||
PARTITION BY c.gobject_id, c.attribute_name ORDER BY c.src_pri, c.depth) AS rn
|
||||
FROM candidate c
|
||||
)
|
||||
SELECT
|
||||
r.tag_name + '.' + r.attribute_name AS full_tag_reference,
|
||||
r.tag_name AS source_object_reference
|
||||
FROM ranked r
|
||||
WHERE r.rn = 1
|
||||
AND r.src_pri = 0
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM deployed_package_chain dpc2
|
||||
INNER JOIN primitive_instance pi ON pi.package_id = dpc2.package_id AND pi.primitive_name = r.attribute_name
|
||||
INNER JOIN primitive_definition pd ON pd.primitive_definition_id = pi.primitive_definition_id AND pd.primitive_name = 'AlarmExtension'
|
||||
WHERE dpc2.gobject_id = r.gobject_id
|
||||
)
|
||||
ORDER BY r.tag_name, r.attribute_name";
|
||||
}
|
||||
|
||||
@@ -27,4 +27,12 @@ public interface IGalaxyRepository
|
||||
/// <summary>Retrieves all attributes for Galaxy objects from the repository.</summary>
|
||||
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||
Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves only the alarm-bearing attributes (those whose owning object
|
||||
/// configures an <c>AlarmExtension</c> primitive) for building the
|
||||
/// subtag-fallback watch-list.
|
||||
/// </summary>
|
||||
/// <param name="ct">Token to cancel the asynchronous operation.</param>
|
||||
Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using ZB.MOM.WW.MxGateway.Server.Galaxy;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Galaxy;
|
||||
|
||||
/// <summary>
|
||||
/// Pure mapper tests for <see cref="GalaxyRepository.MapAlarmRow"/>. These assert the
|
||||
/// FullTagReference / SourceObjectReference derivation produced by
|
||||
/// <c>AlarmAttributesSql</c> without touching a database: the SQL projects
|
||||
/// <c>tag_name</c> as the source object and <c>tag_name + '.' + attribute_name</c> as
|
||||
/// the full reference, exactly as <c>AttributesSql</c> does.
|
||||
/// </summary>
|
||||
public sealed class GalaxyAlarmAttributeMappingTests
|
||||
{
|
||||
/// <summary>Verifies the mapper copies both projected columns onto the row.</summary>
|
||||
[Fact]
|
||||
public void MapAlarmRow_CopiesProjectedColumns()
|
||||
{
|
||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
||||
fullTagReference: "Tank01.Level.HiHi",
|
||||
sourceObjectReference: "Tank01");
|
||||
|
||||
Assert.Equal("Tank01.Level.HiHi", row.FullTagReference);
|
||||
Assert.Equal("Tank01", row.SourceObjectReference);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies <see cref="GalaxyAlarmAttributeRow.AckCommentSubtag"/> is always empty:
|
||||
/// the schema does not expose an ack-comment address, so the watch-list resolver
|
||||
/// composes it later from configuration.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void MapAlarmRow_LeavesAckCommentSubtagEmpty()
|
||||
{
|
||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(
|
||||
fullTagReference: "Tank01.Level.HiHi",
|
||||
sourceObjectReference: "Tank01");
|
||||
|
||||
Assert.Equal(string.Empty, row.AckCommentSubtag);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the SourceObjectReference is the owning object (the SQL <c>tag_name</c>),
|
||||
/// i.e. the segment that precedes the first attribute dot in the full reference, even
|
||||
/// when the attribute itself is a multi-segment extension path.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData("Tank01", "Level.HiHi", "Tank01.Level.HiHi")]
|
||||
[InlineData("Pump_001", "Speed", "Pump_001.Speed")]
|
||||
[InlineData("TestAlarm001", "Alarm001", "TestAlarm001.Alarm001")]
|
||||
public void MapAlarmRow_SourceObjectIsSegmentBeforeFirstAttributeDot(
|
||||
string tagName,
|
||||
string attributeName,
|
||||
string expectedFullReference)
|
||||
{
|
||||
// Mirror the AlarmAttributesSql projection: full_tag_reference = tag_name + '.' + attribute_name.
|
||||
string fullTagReference = tagName + "." + attributeName;
|
||||
|
||||
GalaxyAlarmAttributeRow row = GalaxyRepository.MapAlarmRow(fullTagReference, tagName);
|
||||
|
||||
Assert.Equal(expectedFullReference, row.FullTagReference);
|
||||
Assert.Equal(tagName, row.SourceObjectReference);
|
||||
Assert.Equal(row.FullTagReference, row.SourceObjectReference + "." + attributeName);
|
||||
}
|
||||
}
|
||||
@@ -378,6 +378,10 @@ public sealed class GalaxyHierarchyCacheTests : IDisposable
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAttributeRow>> GetAttributesAsync(CancellationToken ct = default)
|
||||
=> throw new InvalidOperationException("GetAttributesAsync should not be reached");
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
=> throw new InvalidOperationException("GetAlarmAttributesAsync should not be reached");
|
||||
}
|
||||
|
||||
/// <summary>Snapshot store whose <see cref="SaveAsync"/> cancels the token mid-save.</summary>
|
||||
@@ -465,6 +469,10 @@ public sealed class GalaxyHierarchyCacheTests : IDisposable
|
||||
GetAttributesCount++;
|
||||
return Task.FromResult(_attributes);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(new List<GalaxyAlarmAttributeRow>());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -518,6 +526,10 @@ public sealed class GalaxyHierarchyCacheTests : IDisposable
|
||||
GetAttributesCount++;
|
||||
throw toThrow;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<List<GalaxyAlarmAttributeRow>> GetAlarmAttributesAsync(CancellationToken ct = default)
|
||||
=> throw toThrow;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user