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:
Joseph Doherty
2026-10-11 02:30:25 -04:00
parent 13d628a07f
commit d2be213e86
9 changed files with 330 additions and 19 deletions
@@ -345,6 +345,51 @@ public sealed class HistoryPagingTests
store.TryTake(null, cp3).ShouldNotBeNull(); // retained
}
[Fact]
public void SliceTieCluster_forward_drain_steps_by_the_backend_resolution()
{
// A microsecond backend (QuestDB, 10 ticks) truncates T + 1 tick back to T, so the drained cluster must be
// stepped off by the resolution, not by one tick, or the same sample comes back forever.
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
HistoryPaging.SliceTieCluster(
clusterCount: 1, skip: 1, cap: 1, boundaryT: t, endUtc: t.AddHours(1),
out _, out var sliceCount, out var nextStartUtc, out var nextSkip, resolutionTicks: 10);
sliceCount.ShouldBe(0);
nextStartUtc.ShouldBe(t.AddTicks(10));
nextSkip.ShouldBe(0);
}
[Fact]
public void SliceTieCluster_reverse_drain_steps_down_by_the_backend_resolution()
{
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
HistoryPaging.SliceTieCluster(
clusterCount: 1, skip: 1, cap: 1, boundaryT: t, endUtc: t.AddHours(-1),
out _, out _, out var nextStartUtc, out _, reverse: true, resolutionTicks: 10);
nextStartUtc.ShouldBe(t.AddTicks(-10));
}
[Fact]
public void SliceTieCluster_forward_drain_terminates_when_one_resolution_step_passes_the_end()
{
var t = new DateTime(2026, 1, 1, 0, 0, 5, DateTimeKind.Utc);
HistoryPaging.SliceTieCluster(
clusterCount: 1, skip: 1, cap: 1, boundaryT: t, endUtc: t.AddTicks(9),
out _, out _, out var nextStartUtc, out _, resolutionTicks: 10);
nextStartUtc.ShouldBeNull();
}
[Fact]
public void SliceTieCluster_rejects_a_resolution_below_one() =>
Should.Throw<ArgumentOutOfRangeException>(() => HistoryPaging.SliceTieCluster(
1, 1, 1, DateTime.UtcNow, DateTime.UtcNow.AddHours(1), out _, out _, out _, out _, resolutionTicks: 0));
private static HistoryContinuationState RawState() => new(
"WW.Tag", DateTime.UtcNow, DateTime.UtcNow.AddHours(1),
BoundarySkipCount: 1, NumValuesPerNode: 50);