Merge feat/668-modified-values: Part 11 modified-value history (IsReadModified) on SqlServer + QuestDB (#668)
This commit is contained in:
+77
-4
@@ -72,6 +72,50 @@ public sealed class HistoryUpdateOverTheWireTests
|
||||
finally { await rig.DisposeAsync(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Part 11 modified-value history over the wire (#668): two inserts and a delete by the key principal, then a
|
||||
/// HistoryReadRaw with <c>IsReadModified = true</c> decodes as a <see cref="HistoryModifiedData"/> whose
|
||||
/// <c>ModificationInfos</c> carry the update type and the principal, and whose values carry the PRIOR value.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task A_modified_read_returns_the_trail_with_principal_and_update_type_over_the_wire()
|
||||
{
|
||||
var ct = TestContext.Current.CancellationToken;
|
||||
var store = new SharedStore();
|
||||
var rig = await StartAsync("HuWireModified", encrypted: false, ct);
|
||||
try
|
||||
{
|
||||
rig.Nm.HistorianDataUpdater = store;
|
||||
rig.Nm.HistorianDataSource = store;
|
||||
rig.Enforce(true, [Key()], ClusterGrant(KeyPrincipal, "acl-editor", Editor));
|
||||
var id = rig.EnsureRawVariable("Speed", "Double", writable: false, Tagname);
|
||||
|
||||
using var session = await rig.ConnectAsync(anonymous: false, ct);
|
||||
var node = RawNode(session, id);
|
||||
await UpdateAsync(session, node, PerformUpdateType.Insert, ct, (T1, 1.5), (T2, 2.5));
|
||||
await DeleteAtTimeAsync(session, node, ct, T1);
|
||||
|
||||
var details = new ReadRawModifiedDetails
|
||||
{
|
||||
StartTime = T1.AddHours(-1), EndTime = T1.AddHours(1), NumValuesPerNode = 100, IsReadModified = true,
|
||||
};
|
||||
var response = await session.HistoryReadAsync(
|
||||
null, new ExtensionObject(details), TimestampsToReturn.Both, false,
|
||||
new HistoryReadValueIdCollection { new HistoryReadValueId { NodeId = node } }, ct);
|
||||
var result = response.Results.ShouldHaveSingleItem();
|
||||
result.StatusCode.Code.ShouldBe(StatusCodes.Good);
|
||||
var data = ExtensionObject.ToEncodeable(result.HistoryData).ShouldBeOfType<HistoryModifiedData>();
|
||||
|
||||
data.DataValues.Select(v => v.SourceTimestamp).ShouldBe([T1, T1, T2]);
|
||||
data.ModificationInfos.Select(m => m.UpdateType)
|
||||
.ShouldBe([HistoryUpdateType.Insert, HistoryUpdateType.Delete, HistoryUpdateType.Insert]);
|
||||
data.ModificationInfos.ShouldAllBe(m => m.UserName == KeyPrincipal);
|
||||
data.DataValues[0].StatusCode.Code.ShouldBe(StatusCodes.BadNoData); // an insert had no prior value
|
||||
((double)data.DataValues[1].Value).ShouldBe(1.5); // the deleted sample's prior value
|
||||
}
|
||||
finally { await rig.DisposeAsync(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// ALLOW beside DENY: the same server, same node — the editor session is admitted, the session
|
||||
/// without the HistoryUpdate grant gets BadUserAccessDenied and the updater is never called.
|
||||
@@ -263,14 +307,21 @@ public sealed class HistoryUpdateOverTheWireTests
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly SortedDictionary<DateTime, double> _samples = [];
|
||||
private readonly List<HistoryModifiedValue> _trail = [];
|
||||
private int _updateCalls;
|
||||
|
||||
private void Record(DateTime t, double? prior, HistoryModificationType type, HistoryUpdateContext who) =>
|
||||
_trail.Add(new HistoryModifiedValue(
|
||||
new DataValueSnapshot(prior, prior is null ? StatusCodes.BadNoData : StatusCodes.Good, t, t),
|
||||
DateTime.UtcNow, type, who.Principal, _trail.Count + 1));
|
||||
|
||||
public int UpdateCalls => Volatile.Read(ref _updateCalls);
|
||||
|
||||
public int Count { get { lock (_gate) return _samples.Count; } }
|
||||
|
||||
public async Task<IReadOnlyList<uint>> UpdateDataAsync(
|
||||
string tag, HistoryUpdateMode mode, IReadOnlyList<HistorizationValue> values, CancellationToken ct)
|
||||
string tag, HistoryUpdateMode mode, IReadOnlyList<HistorizationValue> values, HistoryUpdateContext context,
|
||||
CancellationToken ct)
|
||||
{
|
||||
Interlocked.Increment(ref _updateCalls);
|
||||
await Task.Delay(1, ct).ConfigureAwait(false);
|
||||
@@ -285,21 +336,43 @@ public sealed class HistoryUpdateOverTheWireTests
|
||||
if (_samples.ContainsKey(t)) { codes.Add(StatusCodes.BadEntryExists); continue; }
|
||||
v.Value.TryGetDouble(out var d).ShouldBeTrue();
|
||||
_samples[t] = d;
|
||||
Record(t, null, HistoryModificationType.Insert, context);
|
||||
codes.Add(StatusCodes.GoodEntryInserted);
|
||||
}
|
||||
}
|
||||
return codes;
|
||||
}
|
||||
|
||||
public Task<uint> DeleteRawAsync(string tag, DateTime startUtc, DateTime endUtc, CancellationToken ct) =>
|
||||
public Task<uint> DeleteRawAsync(string tag, DateTime startUtc, DateTime endUtc, HistoryUpdateContext context,
|
||||
CancellationToken ct) =>
|
||||
throw new NotSupportedException();
|
||||
|
||||
public async Task<IReadOnlyList<uint>> DeleteAtTimeAsync(
|
||||
string tag, IReadOnlyList<DateTime> timesUtc, CancellationToken ct)
|
||||
string tag, IReadOnlyList<DateTime> timesUtc, HistoryUpdateContext context, CancellationToken ct)
|
||||
{
|
||||
await Task.Delay(1, ct).ConfigureAwait(false);
|
||||
lock (_gate)
|
||||
return timesUtc.Select(t => _samples.Remove(t) ? StatusCodes.Good : StatusCodes.BadNoEntryExists).ToList();
|
||||
return timesUtc.Select(t =>
|
||||
{
|
||||
if (!_samples.Remove(t, out var prior)) return StatusCodes.BadNoEntryExists;
|
||||
Record(t, prior, HistoryModificationType.Delete, context);
|
||||
return StatusCodes.Good;
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
public async Task<HistoryModifiedReadResult> ReadModifiedAsync(
|
||||
string fullReference, DateTime startUtc, DateTime endUtc, uint maxValues, HistoryModifiedCursor? resumeAfter,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Delay(1, cancellationToken).ConfigureAwait(false);
|
||||
fullReference.ShouldBe(Tagname);
|
||||
lock (_gate)
|
||||
{
|
||||
var rows = _trail
|
||||
.Where(m => m.Value.SourceTimestampUtc >= startUtc && m.Value.SourceTimestampUtc < endUtc)
|
||||
.OrderBy(m => m.Value.SourceTimestampUtc).ThenBy(m => m.ModifiedId).ToList();
|
||||
return new HistoryModifiedReadResult(rows, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CoreHistoryReadResult> ReadRawAsync(
|
||||
|
||||
Reference in New Issue
Block a user