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
@@ -84,6 +84,31 @@ internal static class Program
return 0;
}
string? dumpTypeName = GetArg(args, "--dump-type-members");
if (dumpTypeName is not null)
{
Type dumpType = GetType(assembly, dumpTypeName);
if (dumpType.IsEnum)
{
var values = Enum.GetValues(dumpType).Cast<object>()
.Select(v => $"{v} = {Convert.ToInt64(v)}").OrderBy(s => s).ToArray();
Console.WriteLine(Serialize(new { Type = dumpType.FullName, EnumValues = values }));
return 0;
}
BindingFlags df = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic;
Console.WriteLine(Serialize(new
{
Type = dumpType.FullName,
Properties = dumpType.GetProperties(df).Select(p => $"{p.PropertyType.Name} {p.Name}").OrderBy(s => s).ToArray(),
Fields = dumpType.GetFields(df).Select(f => $"{f.FieldType.Name} {f.Name}").OrderBy(s => s).ToArray(),
Methods = dumpType.GetMethods(df)
.Where(m => !m.IsSpecialName)
.Select(m => $"{m.ReturnType.Name} {m.Name}({string.Join(", ", m.GetParameters().Select(p => p.ParameterType.Name))})")
.OrderBy(s => s).ToArray(),
}));
return 0;
}
Type accessType = GetType(assembly, "ArchestrA.HistorianAccess");
Type connectionArgsType = GetType(assembly, "ArchestrA.HistorianConnectionArgs");
Type connectionStatusType = GetType(assembly, "ArchestrA.HistorianConnectionStatus");
@@ -224,6 +249,41 @@ internal static class Program
SetProperty(queryArgs, "EventOrder", Enum.Parse(eventOrderType, "Ascending"));
snapshots["EventQueryArgsBeforeStart"] = SnapshotObject(queryArgs);
// R1.7 event-filter capture: --event-filter "Property:Op:Value" (repeatable via ';').
// Calls EventQuery.AddEventFilter(name, HistorianComparisionType, value, out err) so the
// filter predicate rides StartEventQuery's request buffer for instrument-wcf capture.
string? eventFilterSpec = GetArg(args, "--event-filter");
if (!string.IsNullOrWhiteSpace(eventFilterSpec))
{
Type comparisonType = GetType(assembly, "ArchestrA.HistorianComparisionType");
MethodInfo addFilterMethod = queryType.GetMethod("AddEventFilter",
new[] { typeof(string), comparisonType, typeof(object), errorType.MakeByRefType() })
?? throw new MissingMethodException("EventQuery.AddEventFilter");
foreach (string clause in eventFilterSpec!.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries))
{
string[] parts = clause.Split(new[] { ':' }, 3);
if (parts.Length < 3)
{
throw new ArgumentException($"--event-filter clause '{clause}' must be Property:Op:Value.");
}
object filterError = Activator.CreateInstance(errorType)!;
object?[] addFilterArgs = [parts[0], Enum.Parse(comparisonType, parts[1], ignoreCase: true), parts[2], filterError];
object addFilterResult = addFilterMethod.Invoke(query, addFilterArgs)!;
filterError = addFilterArgs[3]!;
rows.Add(new
{
Kind = "AddEventFilter",
Property = parts[0],
Op = parts[1],
Value = parts[2],
FilterId = addFilterResult,
ErrorDescription = GetPropertyText(filterError, "ErrorDescription"),
});
}
snapshots["EventQueryAfterAddFilter"] = SnapshotObject(query);
}
startError = Activator.CreateInstance(errorType)!;
MethodInfo startMethod = queryType.GetMethod("StartQuery", new[] { eventQueryArgsType, errorType.MakeByRefType() })
?? throw new MissingMethodException("EventQuery.StartQuery");