R1.7: server-side event filters — ReadEventsAsync(HistorianEventFilter), live-honored

Roadmap M1 R1.7. Filters are set on the native EventQuery object via
AddEventFilter(property, HistorianComparisionType, value) — NOT EventQueryArgs
(time/count/order only). Found via a new harness --dump-type-members command.

Captured the native filtered StartEventQuery pRequestBuff (Capture-EventFilter.ps1 +
harness --event-filter knob) and diffed Equal(0) vs Contains(12) to isolate the
operator field. Filter block (decoded byte-for-byte):
  ushort 0 + uint filterCount + uint condCount + uint nameLen + name(UTF-16) +
  uint 1 + ushort op + uint 1 + value(0x09-LEN-0x00 compact-ASCII) + byte 0

The filter is REAL, not inert (unlike the analog-summary knobs): a non-matching
predicate returns 0 events; Type=Equal=User.Write returns only User.Write events.
Verified live via both the native harness and the SDK.

- HistorianClient.ReadEventsAsync(start, end, HistorianEventFilter, ct) overload
- HistorianEventFilter + HistorianEventComparison (18 ops, ordinals = native)
- Filter encoding in HistorianEventQueryProtocol (empty-filter path unchanged)
- Golden-byte tests (block match, op field, empty-filter regression) + gated live test

Single string-valued predicate only; multi-filter (OR) / multi-condition (AND via
AddEventFilterCondition) framing is partially captured and not shipped. 216 unit
tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-20 18:32:03 -04:00
parent f1e23a3a02
commit 6d470eab4a
12 changed files with 517 additions and 18 deletions
+19 -1
View File
@@ -90,7 +90,25 @@ public sealed class HistorianClient : IAsyncDisposable
CancellationToken cancellationToken = default)
{
ValidateTimeRange(startUtc, endUtc);
return _protocol.ReadEventsAsync(startUtc, endUtc, cancellationToken);
return _protocol.ReadEventsAsync(startUtc, endUtc, filter: null, cancellationToken);
}
/// <summary>
/// Reads events in the time window, server-filtered by a single predicate
/// (<paramref name="filter"/>) — e.g. <c>Type Equal "User.Write"</c> or
/// <c>Area Contains "Tank"</c>. The historian applies the filter and returns only matching
/// events. Filtering is a real server-side operation (live-verified: a non-matching predicate
/// returns zero events). Single string-valued predicates only; see <see cref="HistorianEventFilter"/>.
/// </summary>
public IAsyncEnumerable<HistorianEvent> ReadEventsAsync(
DateTime startUtc,
DateTime endUtc,
HistorianEventFilter filter,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(filter);
ValidateTimeRange(startUtc, endUtc);
return _protocol.ReadEventsAsync(startUtc, endUtc, filter, cancellationToken);
}
/// <summary>