feat(opcua): HistoryReadEvents projects EventType, ConditionName, SourceNode; honours the where-clause (lmxopcua#746)
HistoryReadEvents projected only EventId / SourceName / Time / ReceiveTime / Message / Severity: a select clause asking for EventType, ConditionName or SourceNode (what most generic alarm viewers ask for) got nulls, and the event filter's where-clause (an OfType) was ignored. - HistoricalEvent gains optional EventTypeName, ConditionName and AlarmId. SqlServer, QuestDB and Gateway event reads fill them from what their alarm writers already store (AlarmTypeName / AlarmName / AlarmId; the gateway event Type and its AlarmName / AlarmId properties). - The node manager maps the type name to its Part 9 event type (unknown or non-event => AlarmConditionType), projects ConditionName, and resolves SourceNode to the materialised condition the alarm id names (the live events' SourceNode), else null. - The where-clause is evaluated per event with the SDK content filter through an IFilterTarget (OfType via the type tree; other operators against the projected fields), after the page's cursor is taken, so paging is unchanged. Tested in the node manager, over a real client session (the Client.CLI has no `historyread --events` mode yet; filed as a follow-up), and by alarm writer -> event read round trips on SqlServer and QuestDB. Rebased from the unpushed remed/lane-H2 branch (originally authored 2026-10-22 in session_01Ub9hGmYLQvn9XETzw7CMsT) onto current master. Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
@@ -33,9 +33,16 @@ internal static class EventMapper
|
||||
EventTimeUtc: historianEvent.EventTime?.ToDateTime() ?? default, // Utc kind
|
||||
ReceivedTimeUtc: historianEvent.ReceivedTime?.ToDateTime() ?? default, // Utc kind
|
||||
Message: message,
|
||||
Severity: ParseSeverity(historianEvent.Properties));
|
||||
Severity: ParseSeverity(historianEvent.Properties),
|
||||
// What GatewayAlarmHistorianWriter sent (AlarmEventMapper): Type = AlarmTypeName, AlarmName / AlarmId properties.
|
||||
EventTypeName: string.IsNullOrEmpty(historianEvent.Type) ? null : historianEvent.Type,
|
||||
ConditionName: Property(historianEvent.Properties, "AlarmName"),
|
||||
AlarmId: Property(historianEvent.Properties, "AlarmId"));
|
||||
}
|
||||
|
||||
private static string? Property(IDictionary<string, string> properties, string name) =>
|
||||
properties.TryGetValue(name, out var v) && !string.IsNullOrEmpty(v) ? v : null;
|
||||
|
||||
/// <summary>Maps a batch of gateway events to historical events, in order.</summary>
|
||||
/// <param name="events">The gateway wire events.</param>
|
||||
/// <returns>The driver-agnostic historical events.</returns>
|
||||
|
||||
+5
-2
@@ -449,7 +449,7 @@ public sealed class QuestDbHistorianDataSource : IHistorianDataSource
|
||||
var (lo, hi) = start <= end ? (start, end) : (end, start);
|
||||
var bounded = maxEvents > 0 && maxEvents < int.MaxValue;
|
||||
var sql =
|
||||
$"SELECT event_id, source, ts, received_utc, message, severity FROM {_conn.EventTable} " +
|
||||
$"SELECT event_id, source, ts, received_utc, message, severity, alarm_type_name, alarm_name, alarm_id FROM {_conn.EventTable} " +
|
||||
(reverse ? "WHERE ts > $1 AND ts <= $2" : "WHERE ts >= $1 AND ts < $2") +
|
||||
(sourceName is null ? "" : " AND source = $3") +
|
||||
(reverse ? " ORDER BY ts DESC, event_id DESC" : " ORDER BY ts ASC, event_id ASC") +
|
||||
@@ -474,7 +474,10 @@ public sealed class QuestDbHistorianDataSource : IHistorianDataSource
|
||||
eventTime,
|
||||
reader.IsDBNull(3) ? eventTime : QuestDbConnection.ReadUtc(reader, 3),
|
||||
reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||
HistoricalEventRows.ClampSeverity(reader.IsDBNull(5) ? 0 : reader.GetInt32(5)));
|
||||
HistoricalEventRows.ClampSeverity(reader.IsDBNull(5) ? 0 : reader.GetInt32(5)),
|
||||
EventTypeName: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(6) ? null : reader.GetString(6)),
|
||||
ConditionName: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(7) ? null : reader.GetString(7)),
|
||||
AlarmId: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(8) ? null : reader.GetString(8)));
|
||||
if (bounded && events.Count == maxEvents)
|
||||
{
|
||||
continuation = Encoding.UTF8.GetBytes(EventContinuation.Encode(e.EventTimeUtc, e.EventId));
|
||||
|
||||
@@ -7,4 +7,9 @@ public static class HistoricalEventRows
|
||||
/// <param name="severity">Stored severity.</param>
|
||||
/// <returns>The severity clamped to 0..65535.</returns>
|
||||
public static ushort ClampSeverity(int severity) => (ushort)Math.Clamp(severity, 0, ushort.MaxValue);
|
||||
|
||||
/// <summary>A stored optional text column as the event carries it: empty (the writers' "none") is null.</summary>
|
||||
/// <param name="value">The stored text.</param>
|
||||
/// <returns>The text, or null when empty or absent.</returns>
|
||||
public static string? NullIfEmpty(string? value) => string.IsNullOrEmpty(value) ? null : value;
|
||||
}
|
||||
|
||||
+5
-2
@@ -396,7 +396,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
var (lo, hi) = startUtc <= endUtc ? (startUtc, endUtc) : (endUtc, startUtc);
|
||||
var bounded = maxEvents > 0 && maxEvents < int.MaxValue;
|
||||
var sql =
|
||||
$"SELECT {(bounded ? "TOP (@take) " : "")}EventId, SourceName, EventTimeUtc, ReceivedTimeUtc, Message, Severity " +
|
||||
$"SELECT {(bounded ? "TOP (@take) " : "")}EventId, SourceName, EventTimeUtc, ReceivedTimeUtc, Message, Severity, AlarmTypeName, AlarmName, AlarmId " +
|
||||
$"FROM {_conn.Q("Event")} WHERE " +
|
||||
(reverse ? "EventTimeUtc > @lo AND EventTimeUtc <= @hi" : "EventTimeUtc >= @lo AND EventTimeUtc < @hi") +
|
||||
(sourceName is null ? "" : " AND SourceName = @source") +
|
||||
@@ -421,7 +421,10 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
SqlHistorianConnection.GetUtc(reader, 2),
|
||||
SqlHistorianConnection.GetUtc(reader, 3),
|
||||
reader.IsDBNull(4) ? null : reader.GetString(4),
|
||||
HistoricalEventRows.ClampSeverity(reader.GetInt32(5)));
|
||||
HistoricalEventRows.ClampSeverity(reader.GetInt32(5)),
|
||||
EventTypeName: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(6) ? null : reader.GetString(6)),
|
||||
ConditionName: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(7) ? null : reader.GetString(7)),
|
||||
AlarmId: HistoricalEventRows.NullIfEmpty(reader.IsDBNull(8) ? null : reader.GetString(8)));
|
||||
if (bounded && events.Count == maxEvents)
|
||||
{
|
||||
continuation = Encoding.UTF8.GetBytes(EventContinuation.Encode(e.EventTimeUtc, e.EventId));
|
||||
|
||||
Reference in New Issue
Block a user