Add velocity-backend-jdbi: the Postgres tumbling reference backend - #8
Merged
Conversation
The first real velocity-spi backend (v1 tumbling reference, P10/§8) — Postgres via JDBI, validated against a real Postgres with Testcontainers. - JdbiVelocityBackend implements CountStore/SumStore/DistinctStore + TumblingSupport + SeedSupport. Exact, ATOMIC read-your-write: each apply is one transaction whose per-bucket upsert (INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING) returns the post-write value, so concurrent applies never lose an increment (NFR-6, acc. #1). - Backend owns bucketing + the clock (ADR 0003/FR-3): a tumbling window resolves to the aligned current bucket floor(nowMs/Dms)*Dms — byte-identical to velocity-testkit WindowMath, so a JDBI value equals the in-memory reference. Storage identity is (namespace, subject, aggregation[dimension + type-via-table], window, bucket) — NOT feature name (a QueryTuple carries none), matching InMemory's StoreKey. - Three per-aggregation tables (counts / sums NUMERIC(38,0) cents / distinct_members BYTEA, exact). Idempotent CREATE ... IF NOT EXISTS migrate(). Exact-only distinct (HLL degradation is a documented follow-up). Unsupported (sliding) window -> Failure(UNSUPPORTED_WINDOW) on apply and query, never a silent 0. HLL seed rejected. - No Dagger/Dropwizard/Jackson (a backend depends only on velocity-spi + JDBI/Postgres/ Hikari/slf4j). Tests (real Postgres via Testcontainers, Docker required): 33 tests, 0 skipped. Wires the tumbling-compatible SHARED TCK scenarios (TumblingScenarios, SeedSupportScenarios, CapabilityConformanceScenarios) + a 16-thread concurrency test asserting exact atomicity (acc. #1) + bespoke tumbling Count/Sum/Distinct/Purge tests. Coverage LINE 100% / BRANCH 97.4% (gate 80/70). Validated by the change-validator agent: VERDICT APPROVE (round-trip fix confirmed, concurrency atomic, bespoke tests NOT weaker than the shared scenarios). FOLLOW-UP (before velocity-backend-redis): parameterize the window-agnostic TCK scenarios (Count/Sum/Distinct/Purge currently hardcode a sliding window) so every backend runs the SHARED scenarios instead of bespoke ones — see PR body. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
velocity-backend-jdbi— the first realvelocity-spibackend (the v1 Postgres/JDBI tumblingreference, P10/§8), proven against a real Postgres via Testcontainers. This is the template every
future backend copies.
What's in it
JdbiVelocityBackendimplementsCountStore/SumStore/DistinctStore+TumblingSupport+SeedSupport. Exact,ATOMICread-your-write: eachapplyis one transaction whose per-bucketupsert (
INSERT ... ON CONFLICT ... DO UPDATE ... RETURNING) returns the post-write value, soconcurrent applies never lose an increment (NFR-6, acceptance Bump actions/setup-java from 5.5.0 to 5.6.0 in the gh-actions group #1).
current bucket
floor(nowMs/Dms)·Dms— byte-identical tovelocity-testkit'sWindowMath, so aJDBI value equals the in-memory reference. Storage identity is
(namespace, subject, aggregation, window, bucket)— not feature name (aQueryTuplecarries none), matching the in-memoryStoreKey.NUMERIC(38,0)cents / distinct_membersBYTEA, exact).Idempotent
migrate(). Exact-only distinct (HLL degradation is a documented follow-up). Anunsupported (sliding) window →
Failure(UNSUPPORTED_WINDOW)on both paths, never a silent 0;HLL seed rejected. No Dagger/Dropwizard/Jackson.
Tests (real Postgres via Testcontainers)
33 tests, 0 skipped, LINE 100% / BRANCH 97.4% (gate 80/70). Includes:
TumblingScenarios(bucket reset),SeedSupportScenarios(seeded≡recorded, acceptance #16; HLL/unsupported rejects),CapabilityConformanceScenarios(declaration↔impl agreement, unsupported→Failure,distinctHllSliding=false);upsert — acceptance Bump actions/setup-java from 5.5.0 to 5.6.0 in the gh-actions group #1);
Review
change-validatoragent: VERDICT APPROVE. It confirmed the round-trip fix (identical key on bothpaths; bucketing byte-identical to
WindowMath), the concurrency test's exact atomicity, tumbling-onlycontract fidelity, no forbidden imports — and diffed the bespoke tests method-by-method against the
shared scenarios, confirming they assert the same facets over tumbling and are not weaker.
velocity-backend-redis(tracked)The window-agnostic TCK scenarios (
Count/Sum/Distinct/PurgeStoreScenarios) currently hardcode asliding window, so a tumbling-only backend can't run them as-authored — which is why this PR uses
bespoke tumbling tests for those four aggregations. The fix is to parameterize/split those scenarios
by window in
velocity-testkitso every backend runs the shared conformance scenarios (this alsosubsumes two minor coverage nits the reviewer noted: SUM/DISTINCT cross-namespace isolation and the
query-side unsupported-window path are only exercised via the COUNT store today, though the key/guard
code is shared).
CountStoreScenariosgenuinely mixes three windows, so this is a deliberate TCKrefactor — do it before Redis so Redis inherits the shared harness.
./gradlew :velocity-backend-jdbi:build→ BUILD SUCCESSFUL (Testcontainers postgres:16-alpine).🤖 Generated with Claude Code