fix(historian): page by the backend's timestamp resolution; QuestDB AtTime stamps the requested time
IHistorianDataSource.TimestampResolutionTicks (default 1; QuestDB 10). A drained tie cluster now steps off T by the resolution, so a forward one-value-per-page read on a microsecond backend no longer re-reads the same sample forever (C1). QuestDB AtTime results carry the client's original request time (M3); regression for the NEXT seek skipping a tombstone (M1). Conformance backend wires the real writer and updater. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -212,25 +212,34 @@ internal static class HistoryPaging
|
||||
/// after <paramref name="endUtc"/> (the reverse window excludes <c>end</c>; a resume at exactly <c>end</c>
|
||||
/// would turn into a <c>start == end</c> point read and return the excluded sample). Forward is unchanged.
|
||||
/// </param>
|
||||
/// <param name="resolutionTicks">
|
||||
/// The backend's storage timestamp resolution (<c>IHistorianDataSource.TimestampResolutionTicks</c>, >= 1). A
|
||||
/// drained cluster steps off <paramref name="boundaryT"/> by this many ticks rather than by one: a backend that
|
||||
/// truncates its inputs to its resolution (QuestDB, 10 ticks) would otherwise truncate <c>T + 1 tick</c> back
|
||||
/// to <c>T</c> and return the drained sample again, forever. Lossless for the same reason one tick is at tick
|
||||
/// precision: no stored timestamp lies strictly between <c>T</c> and <c>T ± resolution</c>. The default
|
||||
/// <c>1</c> keeps the tick-precision behaviour exactly.
|
||||
/// </param>
|
||||
public static void SliceTieCluster(
|
||||
int clusterCount, int skip, uint cap, DateTime boundaryT, DateTime endUtc,
|
||||
out int sliceStart, out int sliceCount, out DateTime? nextStartUtc, out int nextSkip,
|
||||
bool reverse = false)
|
||||
bool reverse = false, long resolutionTicks = 1)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfLessThan(resolutionTicks, 1L);
|
||||
sliceStart = Math.Min(skip, clusterCount);
|
||||
sliceCount = Math.Min((int)cap, clusterCount - sliceStart);
|
||||
var emitted = sliceStart + sliceCount;
|
||||
if (emitted < clusterCount) { nextStartUtc = boundaryT; nextSkip = emitted; }
|
||||
else if (reverse)
|
||||
{
|
||||
// DateTime.MinValue cannot step down; nothing can lie below it anyway.
|
||||
var more = boundaryT > DateTime.MinValue && boundaryT.AddTicks(-1) > endUtc;
|
||||
nextStartUtc = more ? boundaryT.AddTicks(-1) : null;
|
||||
// Nothing can lie below DateTime.MinValue, so a boundary within one resolution step of it cannot step down.
|
||||
var more = boundaryT.Ticks >= resolutionTicks && boundaryT.AddTicks(-resolutionTicks) > endUtc;
|
||||
nextStartUtc = more ? boundaryT.AddTicks(-resolutionTicks) : null;
|
||||
nextSkip = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var next = boundaryT.AddTicks(1);
|
||||
var next = boundaryT.AddTicks(resolutionTicks);
|
||||
if (next <= endUtc) { nextStartUtc = next; nextSkip = 0; }
|
||||
else { nextStartUtc = null; nextSkip = 0; }
|
||||
}
|
||||
|
||||
@@ -5931,7 +5931,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2
|
||||
HistoryPaging.SliceTieCluster(
|
||||
cluster.Count, boundarySkip, numValuesPerNode, startUtc, endUtc,
|
||||
out var sliceStart, out var sliceCount, out var nextStartUtc, out var nextSkip,
|
||||
reverse: reverse);
|
||||
reverse: reverse, resolutionTicks: Math.Max(1L, HistorianDataSource.TimestampResolutionTicks));
|
||||
|
||||
var slice = new List<DataValueSnapshot>(sliceCount);
|
||||
for (var i = sliceStart; i < sliceStart + sliceCount; i++) slice.Add(cluster[i]);
|
||||
|
||||
Reference in New Issue
Block a user