- migrate auth, manager, data, and agentic persistence to R2DBC
- hard cut pagination, facade, gateway, web, and CLI contracts
- remove legacy database and TSDB adapters and document validation gates
BREAKING CHANGE: platform persistence now requires PostgreSQL 18 with TimescaleDB and the new reactive API contracts.
Phase 3 of docs/design/tsdb-abstraction.md: the store family reaches four
certified adapters, the selection guide publishes the real capability
matrix, and external stores become first-class compose services.
dc3-tsdb-influxdb (3.11.2-core) talks the documented v3 HTTP APIs
directly — line protocol writes, query_sql CSV reads, zero client
dependencies. Integer fields carry the i suffix from the first write (a
bare number binds the column to Float64 forever and destroys ns
timestamps); timestamps cross the wire as CAST(... AS BIGINT)
nanoseconds because JSON renders them in scientific notation; PERCENTILE
is approximate-only and deleteRange does not exist in Core — both
declared honestly (false), the analytics facade computes exact
percentiles from bounded pulls.
dc3-tsdb-iotdb (2.0.10-standalone) maps series onto tree paths
root.dc3.t<tenant>.d<device>.p<point> — path nodes cannot be purely
numeric, so the design's raw-id mapping is amended. The server must run
with timestamp_precision=us and dn_rpc_address=0.0.0.0 (a two-line
properties override shipped under dc3/dependencies/iotdb); WHERE time
literals must use the 2026-08-20T12:59:59+00:00 form because bare epoch
numbers are parsed as milliseconds regardless of the server precision —
a microsecond literal silently matches nothing. Sessions disable
redirection: node discovery hands out internal cluster addresses that
break behind port mappings. GROUP BY LEVEL columns come back
wildcard-shaped and are parsed per column; COUNT rides the INT32 quality
measurement (never null); null measurements are omitted per record — a
null value in insertRecords makes the whole record vanish.
Capability matrix published in docs/tsdb-stores.md per adapter-declared
values (skip counts in the TCK are declared-unsupported cases, degraded
by facades, never wrong data). External store services (tdengine,
influxdb, iotdb) join the optional compose stack with healthchecks and
DC3_TSDB_* passthrough in the app stack.
Gates: timescale 24/24, tdengine 24/24 (2 skips), influxdb 24/24 (2
skips), iotdb 24/24 (3 skips); full-repo compile green.
dc3-tsdb-tdengine maps the port onto TDengine 3.x: one supertable
point_value tagged by (tenant, device, point), one deterministic subtable
pv_<t>_<d>_<p> auto-created on first insert via USING TAGS, database
created with PRECISION 'us' and KEEP 180. Connection settings live under
dc3.tsdb.tdengine.* (REST driver, own Hikari pool); selected by
dc3.tsdb.type=tdengine. Like the broker family, only the default adapter
(timescale) ships inside consumers — switching deployments add this
dependency plus DC3_TSDB_TYPE/DC3_TSDB_TDENGINE_URL.
The supertable mapping grilled four real issues out of the port
surface, each fixed and locked by the TCK:
- Timestamps never touch string form. The REST driver serializes
Timestamp parameters in the client JVM zone while the server parses
them as UTC — every write would shift by the deployment zone and
cursor pagination would drift per page without end. Instants now
travel as epoch-micro integer literals and reads return
CAST(ts AS BIGINT) — verified symmetric against the image.
- AS value collides with a reserved word (agg_value now);
INTERVAL takes a bare number read in the database (micro) precision.
- PERCENTILE only runs on single tables, so single-series percentiles
query the deterministic subtable directly; tenant-wide percentile is
refused rather than approximated.
- REST readiness must be probed with POST /rest/sql + basic auth; the
GET path-style route 404s even on a healthy server.
Honest capabilities: latencyHistogram=false (dashboard degrades to
zero-filled bins via a capability check) and correlation=false (future
analytics facade computes from bucketed pulls); rollups stay NONE until
S16 lands stream computing. TCK result: 23 tests, 0 failures, 0 errors,
2 capability-gated skips; the timescale suite re-ran 23/23 unchanged.
The design doc's capability matrix now reflects the adapter's declared
values instead of the pre-implementation estimates.
First vertical slice of the tsdb abstraction (docs/design/
tsdb-abstraction.md phase 1): the dc3-tsdb top-level family lands with
the S19-final port and a fully certified TimescaleDB adapter.
dc3-tsdb-core — the port, zero store dependencies:
- TsdbModel: SeriesKey, the unified SeriesFilter (single series / series
set / tenant-wide as one shape), PointValueSample with both timestamps
(S9) and the quality flag (S17), AggregateFunction incl. FIRST/LAST
(M4) and gated PERCENTILE, cursor records, analytics records
- TsdbStore SPI: append / last / cursor history / aggregate /
bucketedAggregate / count, the S13 analytics facet
(bucketedCount, countByDimension, lastSeenPerSeries,
latencyHistogram), listSeries, deleteRange, correlation; every read
carries TsdbDeadline
dc3-tsdb-timescale — the reference adapter:
- unnest single-statement batch append (one round trip) with natural
upsert on (series, deviceTime); batches chunked at the declared
maxAppendBatch
- read paths through SeriesFilter-shaped SQL: ROW_NUMBER per-series
last-N, global (create_time, message_id) descending cursor history,
time_bucket bucketed aggregates, percentile_cont, aligned-bucket
corr() correlation, CASE-binned latency histogram
- idempotent schema bootstrap incl. initial-chunk priming (a sentinel
row at a fixed early instant forces TimescaleDB's initial chunk
creation at bootstrap with a controlled boundary)
- capability negotiation logged at startup; rollupSupport NONE in this
extraction (S16 continuous aggregates arrive with phase 2)
dc3-tsdb-tck — the 22-case contract suite on Testcontainers:
append-readback fidelity (every field incl. both timestamps and
quality), newest-first last-N with exact limit, cursor pagination
without skip or duplicate, NULL-skipping aggregates, epoch-anchored
bucket boundaries, series and tenant-wide counts, duplicate-timestamp
last-write-wins, backfill acceptance, cross-tenant isolation,
microsecond precision, 5k-sample burst, the four analytics ops,
multi-series isolation, FIRST/LAST M4, percentile tolerance, quality
round-trip, deadline-bounded reads, and known-correlation detection.
Two debugging lessons worth recording (both fixed and TCK-locked):
- Spring's RowCallbackHandler fires once PER ROW; a while(rs.next())
inside it silently skips every other row — the original cause of all
'vanishing row' symptoms, initially misattributed to TimescaleDB
- TimescaleDB sizes the initial hypertable chunk around the first
inserted row; priming at bootstrap avoids boundary anomalies
Timescale contract suite: 22/22 green against timescale-ha:pg18.