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:
@@ -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.
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user