docs(historian): fold Fable pre-execution review into QuestDB plan + spec (6 blockers, 15 should-fix, 8 nits)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-10 09:09:42 -04:00
parent b3031dfea9
commit f54ab04e64
3 changed files with 228 additions and 18 deletions
@@ -28,7 +28,7 @@ Out of scope:
This is extracted from `Driver.Historian.SqlServer` with **no behaviour change**:
- **`StoredSample`.** It holds the time, the value, the stored status and a deleted flag. It classifies a sample as
- **`StoredSample`.** It holds the time, the value, the stored status. It classifies a sample as
usable, good, bad or uncertain. It applies the quality-contract clamp: a NULL value carrying a non-Bad status is
served as `BadNoData`. It also defines the info-bit masks (`CodeBitsMask`, `SeverityBadBit`, `SeverityMask`).
- **`AtTimeEvaluator`.** This is the former `SqlServerHistorianDataSource.AtTimeValue`. It covers interpolation and
@@ -83,7 +83,7 @@ three are **WAL tables with `DEDUP UPSERT KEYS`**, so re-sending a row with the
```sql
CREATE TABLE IF NOT EXISTS hist_sample (
ts TIMESTAMP, tag SYMBOL, kind BYTE, num DOUBLE, int LONG, str VARCHAR,
ts TIMESTAMP, tag SYMBOL, kind BYTE, num_value DOUBLE, int_value LONG, str_value VARCHAR,
status LONG, deleted BOOLEAN, server_ts TIMESTAMP
) TIMESTAMP(ts) PARTITION BY DAY WAL DEDUP UPSERT KEYS(ts, tag) [TTL n DAYS];
@@ -118,8 +118,8 @@ Column details:
5. Any SQL or transport failure makes the write return `false`, so the recorder retries and the #538 per-tag
quarantine still applies. It never throws.
The recorder is Primary-only (#537), so step 2 does not race another live writer. The recorder does not wait for
WAL visibility.
The recorder is Primary-only (#537), so step 2 does not race another live writer. The recorder waits for WAL
visibility, bounded by `VisibilityTimeout` (plan R5).
### 2.3 HistoryUpdate (soft delete)
@@ -155,8 +155,7 @@ kept forever. `hist_tag` never expires.
Every query filters `NOT deleted`. Every value goes through `StoredSample`, so the quality clamp is the same as SQL
Server's.
- **Raw.** A range on `ts`, `ORDER BY ts [DESC]`, `LIMIT n+1` to detect another page. The dedup key forbids
same-instant ties, so the continuation is just the last timestamp. Bounds follow the existing data-source contract.
- **Raw.** A range on `ts`, `ORDER BY ts [DESC]`, `LIMIT @top` (`maxValuesPerNode == 0` ⇒ unbounded), at most `top` samples, `ContinuationPoint: null`; the node manager pages by re-reading from the last timestamp (plan R8). The dedup key forbids same-instant ties. Bounds follow the existing data-source contract.
- **AtTime.** Find the previous-or-equal neighbour and the next neighbour of each requested time. The default is
`LIMIT 1` seeks on `ts`, batched into chunked statements; T0 picks `ASOF JOIN` for the previous neighbour if it
measures better. `AtTimeEvaluator` then decides the result.
+213 -2
View File
@@ -91,6 +91,67 @@ These are binding on every task and every reviewer.
They are renamed from their `Sql*` names to neutral ones (table in T1). The parity test keys on field names, not
class names.
## Review amendments (Fable, 2026-10-10)
These are binding. They override anything else in this plan that conflicts with them. The full review is
summarised here as B = blocker, S = should-fix, N = nit.
**Global rulings**
- **R4 — resolution units (B2).** Conformance tests express every sub-second offset in units of `B.ResolutionTicks`:
- `AddTicks(k)` becomes `AddTicks(k * B.ResolutionTicks)`.
- Seeds are at least one resolution unit apart.
- A "near miss" is `+ B.ResolutionTicks`, and the last tick of a bucket is `edge.AddTicks(-B.ResolutionTicks)`.
- The 100k-bucket cap test uses `TimeSpan.FromTicks(B.ResolutionTicks)` intervals over a
`100_000 * B.ResolutionTicks` window.
- Every timestamp assertion, including `.Ticks` comparisons and `ServerTimestampUtc`, goes through `B.Round(..)`.
- Fixture bases (`T0`, `T(n)`) stay deliberately not µs-aligned.
- On SQL Server (`ResolutionTicks = 1`) every rewritten expression evaluates to today's literal.
- **R5 — both write paths wait for WAL visibility (B6).** `QuestDbWalVisibility` is a T3 file. The value writer
(T4) and the updater (T6) each call `WaitAsync` once after writing, bounded by `VisibilityTimeout`. A timeout logs a
Warning and does not change the return value or result codes. Spec §2.2's "The recorder does not wait for WAL
visibility" is replaced by "The recorder waits for WAL visibility, bounded by `VisibilityTimeout`."
- **R6 — microsecond domain at the boundary (B3, B4).**
- `QuestDbConnection.Round(DateTime)` (= `ticks - ticks % 10`, UTC kind) is applied to EVERY inbound timestamp:
- Raw and Events window bounds;
- each AtTime request time;
- DeleteRaw bounds and DeleteAtTime times;
- the Processed window, BEFORE `BucketPlan.Create`.
- Results are stamped with the rounded request time.
- `QuestDbConnection.BindTimestamp(cmd, name, DateTime)` is the ONLY way a DateTime reaches Npgsql. It rounds, and it
never sends a sentinel: `DateTime.MinValue` becomes `MinValue.AddTicks(10)`, and `MaxValue` rounds to `MaxValue − 9`
ticks. Binding a raw DateTime is a review FAIL.
- The Global Constraints "Bucket edges" rule therefore holds exactly. T8 documents that a request start with sub-µs
ticks admits a sample stored up to 9 ticks earlier.
- **R7 — one tag authority (B5).** T3 creates `QuestDbTagCatalog.cs`, mirroring `SqlServerTagCatalog`:
- `Task<bool> ExistsAsync(name, ct)`: positive results cached; an invalid name ⇒ false; runs bootstrap `EnsureAsync`
first.
- `Task EnsureAsync(name, byte dataType, ct)`: inserts into `hist_tag` with `ts = 1970-01-01` when absent.
- **The `hist_tag` row is the authority for "known tag".** T4's writer and T6's Insert/Update call `EnsureAsync`.
T5's reads and T6's Replace/DeleteRaw/DeleteAtTime call `ExistsAsync` and never create.
- T4–T6 add no private tag resolver.
- **R8 — Raw paging (S1).** `LIMIT @top`, where `top = maxValuesPerNode == 0 ? int.MaxValue : maxValuesPerNode`. Return
at most `top` samples with `ContinuationPoint: null`, exactly as the SqlServer data source does. The node manager does
the paging (it re-reads from the last timestamp, inclusive). The spec §3 "LIMIT n+1" is wrong and is replaced.
- **R9 — value columns (S3).** In the QuestDB DDL the value columns are `num_value`, `int_value` and `str_value`, to
avoid the reserved word `int`. The `SampleColumns` names in Sql.Common are unchanged.
- **R10 — models (S9, S10).**
- T7 runs on **opus**. It may fix T4/T6 files and the Raw/Events plumbing in `QuestDbHistorianDataSource.cs`.
- A divergence inside `QuestDbAggregateQueries.cs` or the AtTime neighbour logic is reported, not fixed; the
controller dispatches an opus fix.
- T1 runs as **two sequential opus dispatches**:
- **1a:** extract, SqlServer re-point, rename fixes, slnx and parity wiring, moved unit tests. Its gate is the Step 0
counts, reproduced exactly.
- **1b:** the new direct `Sql.Common.Tests`, each falsified.
- Two commits, one review each.
- **R11 — ownership (B1, N6, S8).**
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/**`: T1 in W1, for **mechanical
rename and using fixes only** (no test moved, renamed, re-literalled or deleted; the count stays at baseline), then
T2 in W2.
- `IT/QuestDbConformanceBackend.cs` (seed and read-back only) is owned by **T5**. T7 owns the subclasses
(`QuestDbConformance.cs`).
- `IT/TombstoneTests.Redelivery.cs` is owned by T7 (see the T6 amendment).
- **N4.** `Platforms AnyCPU;x64` goes on both new src csprojs.
---
## Waves, models and parallel safety
@@ -101,7 +162,7 @@ These are binding on every task and every reviewer.
| W1 | T1 extract `Sql.Common` | **opus** |
| W2 | T2 conformance suite over SqlServer ‖ T3 QuestDB skeleton | sonnet ‖ sonnet |
| W3 | T4 writes ‖ T5 reads + aggregates ‖ T6 updater | sonnet ‖ **opus** ‖ sonnet |
| W4 | T7 provider + Host wiring + QuestDB conformance | sonnet |
| W4 | T7 provider + Host wiring + QuestDB conformance | **opus** (R10) |
| W5 | T8 docs → **fable** whole-branch review → one fix wave (**opus**) + scoped re-review (sonnet) → T9 live gate | sonnet / fable / opus / controller |
Reviewers:
@@ -140,6 +201,33 @@ T3 already wired.
### Task 0: QuestDB spike, overlay compose and spec amendments
> **Review amendments (Fable) — binding:**
> - **Q1 is expanded (S2).** Record, with output:
> - the DateTime `Kind`/`NpgsqlDbType` pair that writes `TIMESTAMP` without error (watch for
> `Cannot write DateTime with Kind=UTC to … timestamp without time zone`);
> - whether open → close → reopen on one `NpgsqlDataSource` works without `No Reset On Close=true` (DISCARD ALL);
> - the CLR type returned for `BYTE` (likely `int2`, so use `GetInt16`), `SYMBOL`, `VARCHAR`, `LONG`, `BOOLEAN` and
> `TIMESTAMP`;
> - the parameter typing needed for `$n` in integer arithmetic (`NpgsqlDbType.Bigint`);
> - that a DOUBLE NaN reads as NULL.
> The confirmed keywords go into spec §4 verbatim.
> - **Q3 (S4):** also test `hist_event` with `event_id VARCHAR` as a dedup key. **Fallback:** no dedup on `hist_event`.
> The alarm writer checks `SELECT 1 … WHERE event_id=$1` before inserting, and `ReadEventsAsync` de-duplicates by
> `event_id` client-side.
> - **Q4 fallback (S11):** if `wal_tables()` lacks `writerTxn`/`sequencerTxn`, the visibility wait polls for the written
> key (`WHERE tag=$1 AND ts=$2 AND server_ts=$3`). Record which form is used.
> - **Q5 (S11):** the image must be ≥ 8.2.3, the first release with TTL. Record the floor.
> - **Q7 fallback (S11):** catch a `PostgresException` whose message contains `already exists`, then re-check
> `tables()`. Never swallow blindly.
> - **Q8 fallback (S11):** if neither a window `ORDER BY num` nor the `GROUP BY` expression works, arg-min/arg-max is a
> second statement per node, joined client-side. One-query-per-node is relaxed to two for the ActualTime family only.
> - **Q13 (B4):** bind `DateTime.MinValue`, `DateTime.MaxValue`, `0001-01-01`, `1601-01-01`,
> `9999-12-31T23:59:59.9999999`, and a pre-2000 timestamp with sub-µs ticks, in `WHERE ts >= $1 AND ts < $2`. Record
> what arrives and what reads back. This confirms R6's `BindTimestamp` rule.
> - **Healthcheck (S14):** check whether `curl` exists in the image. If not, use `wget -qO-`, or drop the healthcheck.
> - **Q2 also fixes the page size** that T6 uses for DeleteRaw.
**Goal:** prove every QuestDB behaviour the design assumes, against a pinned image, before any code is built on it.
The findings are written into spec §2–§4.
@@ -240,6 +328,24 @@ Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe"
### Task 1: Extract `Driver.Historian.Sql.Common` (no behaviour change)
> **Review amendments (Fable) — binding:**
> - Runs as dispatch **1a** then **1b** (R10).
> - **Files gain** `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/**`, for mechanical
> rename and using fixes only (R11 / B1). This includes replacing `SqlServerSchemaBootstrapper.FailureCacheDuration`
> with the `BootstrapFailureCache` constant.
> - `ProcessedReadTests.Every_enum_member_is_either_served_or_refused` moves to `Sql.Common.Tests` (B2 list).
> - **Step 1 API additions (S6):**
> - `AlarmEventIds.ToNumericSeverity(AlarmSeverity)` (moved from the private SqlServer method);
> - `public sealed class HistorianHealthCounters` (lock-guarded): `RecordSuccess()`, `RecordFailure(Exception)`
> (`OperationCanceledException` is not a failure), and `Snapshot()` returning `HistorianHealthSnapshot` with the
> `ProcessConnectionOpen = successes > 0 && consecutive == 0` rule. `SqlServerHistorianDataSource` uses it too.
> - `public static class EventContinuation { string Encode(DateTime, string eventId) }`, in the existing
> `"{ticks}|{EventId}"` format.
> - `HistoricalEventRows.ClampSeverity(int) → ushort`.
> - `StatusBits.SeverityUncertainBit = 0x40000000u`.
> - Copy `Platforms AnyCPU;x64` into the new csproj (N4).
**Goal:** move every database-independent piece of `Driver.Historian.SqlServer` into a new shared library. The SQL
Server backend then calls it, with byte-identical behaviour.
@@ -418,6 +524,40 @@ public sealed class BootstrapFailureCache // the 5 s negative-cache mechanism
### Task 2: Conformance suite (shared source), run against SQL Server
> **Review amendments (Fable) — binding:**
> - **AC bullet 1 becomes:** "Every moved test keeps its method name and its expected VALUES/STATUS literals. Tick
> offsets are rewritten per R4. The report lists each test whose offsets were rewritten."
> - **AC bullet 5 becomes:** "A `virtual` override is allowed only for a difference R4 cannot express. The report names
> it, and the controller rules on it before T7."
> - **Step 2 explicit list (B2). Move:**
> - `RawReadTests` — all except `A_missing_schema_surfaces_in_health_LastError_and_is_not_latched`. The SQL tag-count
> line in `Unknown_tag_is_an_empty_result_and_reads_never_create_tags` becomes `B.TagExistsAsync`.
> - `AtTimeReadTests` — all.
> - `ProcessedReadTests` — all except `Every_enum_member_is_either_served_or_refused`, which went to Sql.Common.Tests
> in T1.
> - `QualityContractTests` — all. Direct status-column reads become `B.ReadBackAsync` status checks.
> - `WriteTests` — `Every_kind_round_trips…`, `Writing_the_same_batch_twice_is_idempotent`,
> `Within_batch_duplicate_timestamps…`, `Non_finite_doubles…`, `Empty_batch_succeeds…` and `Tag_is_created…`.
> - `UpdateTests` — all.
> - `LongNameTests` — all except `An_event_read_filtered_by_a_long_source_matches_nothing`.
> - **Stay in SqlServer:**
> - `A_missing_schema…`
> - `Ten_thousand_values_write_in_under_five_seconds`
> - `Concurrent_overlapping_writers…`
> - `A_SQL_failure_returns_false…`
> - `EventReadTests`
> - `ProvisioningTests`
> - `AlarmWriterTests`
> - `SchemaBootstrapIntegrationTests`
> - **Interface (S13):** add `Task<bool> TagExistsAsync(string tag);`. `ReadBackAsync` returns the stored status
> verbatim, with no read-side clamp; say so in its doc comment.
> - **Skip check (S7):** first run with the env var UNSET and confirm the tests are reported Skipped, not Failed.
> Fallback: `ConformanceBase` exposes a lazy, memoised `protected async Task<IConformanceBackend> BackendAsync()`,
> disposed in `DisposeAsync`, and each test starts with `var B = await BackendAsync();`.
> - **Verify (N8):** the report lists every moved method name, plus per-file `[Fact]`/`[Theory]` counts before and
> after.
**Goal:** one backend-neutral suite of hand-computed behaviour tests, compiled into the SqlServer integration project
and passing there. T7 later compiles the same suite into the QuestDB project.
@@ -492,6 +632,19 @@ public abstract class ConformanceBase : IAsyncLifetime
### Task 3: QuestDB skeleton (options, connection, bootstrapper, fixture)
> **Review amendments (Fable) — binding:**
> - **Files gain** `QuestDbTagCatalog.cs` (R7) and `QuestDbWalVisibility.cs` (R5, moved from T6).
> - `QuestDbConnection` gains `Round(DateTime)` and `BindTimestamp` (R6), plus `BindByte` and `ReadKind`, so the T0 Q1
> answers live in one file (S2).
> - The DDL uses `num_value`/`int_value`/`str_value` (R9).
> - **New tests:**
> - `BootstrapTests.ExistsAsync_is_false_for_an_unknown_tag_and_true_after_EnsureAsync`;
> - `Wait_returns_once_writerTxn_reaches_the_captured_sequencerTxn` (or the Q4 fallback form);
> - `Timeout_zero_skips_the_wait`;
> - a `QuestDb.Tests` unit test pinning that `BindTimestamp` never emits `DateTime.MinValue`/`MaxValue`.
> - Copy `Platforms AnyCPU;x64` (N4).
**Goal:** a building `Driver.Historian.QuestDb` project. It has validated options, a pooled Npgsql connection, an
idempotent and concurrency-safe schema bootstrapper with TTL, and an env-gated integration fixture.
@@ -559,6 +712,16 @@ idempotent and concurrency-safe schema bootstrapper with TTL, and an env-gated i
### Task 4: QuestDB writes (value writer, tag provisioner, alarm writer)
> **Review amendments (Fable) — binding:**
> - The writer calls `QuestDbTagCatalog.EnsureAsync` before inserting (R7). After inserting, it calls
> `QuestDbWalVisibility.WaitAsync`; a timeout does not change the return value (R5).
> - **Provisioner (S5):** read the existing `hist_tag` rows per chunk (`WHERE name IN (…)`), merge
> `COALESCE(request, stored)` for `eng_unit` and `description` client-side, then insert. Pin this with the SqlServer
> `Upsert_creates_then_changes_DataType_on_rerun…` literal.
> - The alarm writer uses `AlarmEventIds.ToNumericSeverity`, plus the Q3 fallback if VARCHAR dedup was refused.
> - Every DateTime is bound via `BindTimestamp` (R6).
**Goal:** the recorder, provisioning and alarm-history seams write to QuestDB with the same semantics as SQL Server.
**Files:**
@@ -612,6 +775,22 @@ passes.
### Task 5: QuestDB reads and aggregates
> **Review amendments (Fable) — binding:**
> - **Raw per R8:** `LIMIT @top`, at most `top` samples, `ContinuationPoint: null`. Delete the "LIMIT n+1" text.
> - Every inbound timestamp is rounded via `QuestDbConnection.Round`, and every DateTime is bound via `BindTimestamp`
> (R6). The Processed window is rounded before `BucketPlan.Create`.
> - Tags are resolved through `QuestDbTagCatalog.ExistsAsync` only (R7).
> - **SQL classification (S6):** usable ⇔ `status < {StatusBits.SeverityBadBit}` AND `kind <> 0`; good ⇔ usable AND
> `status < {StatusBits.SeverityUncertainBit}`. The literals are interpolated from the consts, never typed by hand. Note
> that QuestDB's "not Bad" is an unsigned LONG comparison, not T-SQL's `>= 0`.
> - Health uses the shared `HistorianHealthCounters`. Events use `EventContinuation` and
> `HistoricalEventRows.ClampSeverity`.
> - **T5 owns `IT/QuestDbConformanceBackend.cs`** (seed and read-back only; N6). Step 3's throwaway backend IS this file,
> which is kept. Only the throwaway subclasses are deleted.
> - **Extra test (N2):** `ReadTests` includes one seeded Int64-kind row with `num_value = NULL`, proving the kind, not
> the column, decides the value.
**Goal:** `QuestDbHistorianDataSource` serves Raw, AtTime, Events and Processed. Results are identical to SQL Server's,
except for microsecond rounding.
@@ -669,6 +848,18 @@ passes.
### Task 6: QuestDB HistoryUpdate (soft delete) with read-your-writes
> **Review amendments (Fable) — binding:**
> - `QuestDbWalVisibility.cs` moves to T3, so T6 consumes it (R5).
> - Insert and Update call `QuestDbTagCatalog.EnsureAsync`; Replace and the deletes call `ExistsAsync` (R7).
> - **Existence read (S15):** one `WHERE tag=$1 AND ts >= $min AND ts <= $max AND NOT deleted` range query, filtered to
> the winners client-side. Never an `IN` list of `n` parameters.
> - **DeleteRaw (S12):** read live rows in `LIMIT` pages of the Q2 batch size, keyed on the last `ts`, and tombstone them
> page by page. `rowsAffected` is the total.
> - Inbound times are rounded and bound via `BindTimestamp` (R6).
> - The TombstoneTests "redelivery after delete" case moves to T7 as
> `IT/TombstoneTests.Redelivery.cs::Tombstone_is_not_resurrected_by_a_writer_redelivery` (S8). T6 keeps the other two.
**Goal:** `QuestDbHistorianDataUpdater` implements all five operations through insert-only upserts and tombstones. It
returns codes identical to SQL Server's, and waits for WAL visibility before returning.
@@ -730,6 +921,15 @@ passes.
### Task 7: Provider, Host wiring and the conformance suite on QuestDB
> **Review amendments (Fable) — binding:**
> - The model is **opus** (R10). T7 may fix T4/T6 files and the data-source Raw/Events plumbing. Aggregate and AtTime
> divergences are reported to the controller, not fixed.
> - T5 owns `QuestDbConformanceBackend.cs`; T7 owns the subclasses (N6).
> - T7 adds `IT/TombstoneTests.Redelivery.cs` (S8).
> - **Compose default (S14):** the exact Q1-confirmed connection string, with `Host=questdb;Port=8812` substituted,
> keeping every keyword T0 found necessary.
**Goal:** `Backend=QuestDb` resolves, validates and builds all five seams. The full conformance suite passes against
QuestDB.
@@ -782,13 +982,24 @@ QuestDB.
on both backends` with the trailer.
```json:metadata
{"files":["src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbHistorianProvider.cs","tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/QuestDbConformanceBackend.cs","tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/QuestDbConformance.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/HistorianBackendRegistry.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/ServerHistorianOptionsValidator.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj","docker-dev/docker-compose.yml"],"verifyCommand":"both IT projects green with env set; Host historian filters green","acceptanceCriteria":["provider shape","registry + consumer-gated validator","full conformance suite green on QuestDB","compose env"],"modelTier":"standard","model":"sonnet","wave":4}
{"files":["src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb/QuestDbHistorianProvider.cs","tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/QuestDbConformanceBackend.cs","tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.QuestDb.IntegrationTests/QuestDbConformance.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/HistorianBackendRegistry.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/Configuration/ServerHistorianOptionsValidator.cs","src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj","docker-dev/docker-compose.yml"],"verifyCommand":"both IT projects green with env set; Host historian filters green","acceptanceCriteria":["provider shape","registry + consumer-gated validator","full conformance suite green on QuestDB","compose env"],"modelTier":"frontier","model":"opus","wave":4}
```
---
### Task 8: Documentation
> **Review amendments (Fable) — binding:**
> - Document these, in addition to the existing list:
> - R6's sub-µs request-start consequence;
> - QuestDB `LONG` NULL = `long.MinValue`, so a stored `FromInt64(long.MinValue)` reads back Empty (N1);
> - TTL is enforced on write commits only, so a quiet table never drops (N3);
> - all write paths wait for WAL visibility (R5).
> - **CLAUDE.md (N5):** check the Historian section's opening paragraph. If it still says
> "HistorianGateway (sole historian backend)", rewrite it to name all three backends. Otherwise add QuestDb to the
> existing "Pluggable backends" subsection.
**Goal:** the docs describe the QuestDB backend as built, and every claim is checked against the merged code.
**Files:** `docs/Historian.md` (a new §"QuestDB backend"), `docs/Configuration.md` (the `ServerHistorian:QuestDb`
File diff suppressed because one or more lines are too long