Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,24 +1,50 @@
// SPDX-License-Identifier: BSD-3-Clause
package com.codeheadsystems.velocity.backend.jdbi;

import com.codeheadsystems.velocity.spi.model.Window;
import com.codeheadsystems.velocity.spi.model.WindowType;
import com.codeheadsystems.velocity.testkit.tck.CapabilityConformanceScenarios;
import com.codeheadsystems.velocity.testkit.tck.CountStoreScenarios;
import com.codeheadsystems.velocity.testkit.tck.DistinctStoreScenarios;
import com.codeheadsystems.velocity.testkit.tck.PurgeScenarios;
import com.codeheadsystems.velocity.testkit.tck.SeedSupportScenarios;
import com.codeheadsystems.velocity.testkit.tck.SumStoreScenarios;
import com.codeheadsystems.velocity.testkit.tck.TumblingScenarios;
import java.time.Duration;
import org.junit.jupiter.api.Test;

/**
* Drives the {@code velocity-testkit} conformance TCK against real Postgres for the scenarios that
* apply to a tumbling-only backend (ADR 0004): {@link TumblingScenarios}, {@link
* SeedSupportScenarios} and {@link CapabilityConformanceScenarios}. This proves the JDBI backend
* honors the same frozen contract as the reference in-memory backend.
* Drives the full {@code velocity-testkit} conformance TCK against real Postgres for every scenario
* that applies to this tumbling-only backend (ADR 0004) — the same shared {@code *Scenarios} the
* reference in-memory backend runs, now parameterized by window so a tumbling backend supplies its
* own supported windows. This proves the JDBI backend honors the identical frozen contract.
*
* <p>{@code SlidingScenarios} is intentionally NOT run — this backend declares no sliding windows.
* The aggregation suites ({@code Count/Sum/Distinct/PurgeStoreScenarios}) hardcode a sliding window
* in their fixtures, so they cannot run against a tumbling-only backend; the equivalent tumbling
* assertions live in {@link JdbiAggregationTest}.
* The window-agnostic aggregation suites ({@code Count/Sum/Distinct/PurgeStoreScenarios}) run here
* with {@link WindowType#TUMBLING tumbling} windows, including the shared concurrency scenario
* against real Postgres (acceptance #1).
*/
final class JdbiConformanceTckTest extends AbstractJdbiBackendTest {

private static final Window TUMBLING_1M = new Window(Duration.ofMinutes(1), WindowType.TUMBLING);
private static final Window TUMBLING_1H = new Window(Duration.ofHours(1), WindowType.TUMBLING);

private CountStoreScenarios count() {
return new CountStoreScenarios(backend, clock, TUMBLING_1M, TUMBLING_1H);
}

private SumStoreScenarios sum() {
return new SumStoreScenarios(backend, clock, TUMBLING_1H);
}

private DistinctStoreScenarios distinct() {
return new DistinctStoreScenarios(backend, clock, TUMBLING_1H);
}

private PurgeScenarios purge() {
return new PurgeScenarios(backend, TUMBLING_1H);
}

private TumblingScenarios tumbling() {
return new TumblingScenarios(backend, clock);
}
Expand All @@ -31,6 +57,100 @@ private CapabilityConformanceScenarios capability() {
return new CapabilityConformanceScenarios(backend);
}

// ---- CountStoreScenarios (tumbling) ----

@Test
void applyThenQueryReturnsExactCount() {
count().applyThenQueryReturnsExactCount();
}

@Test
void applyResultReflectsWriteReadYourWrite() {
count().applyResultReflectsWriteReadYourWrite();
}

@Test
void applyEmitsOneResultPerWindow() {
count().applyEmitsOneResultPerWindow();
}

@Test
void countValuesIsolatedByNamespace() {
count().valuesIsolatedByNamespace();
}

@Test
void countValuesIsolatedBySubject() {
count().valuesIsolatedBySubject();
}

/** The shared concurrency scenario (acceptance #1) against real Postgres. */
@Test
void concurrentApplyIsAtomic() throws Exception {
count().concurrentApplyIsAtomic();
}

// ---- SumStoreScenarios (tumbling) ----

@Test
void applyThenQueryReturnsExactSumCents() {
sum().applyThenQueryReturnsExactSumCents();
}

@Test
void applyResultReflectsRunningSum() {
sum().applyResultReflectsRunningSum();
}

@Test
void bigDecimalCentsPreservedWithoutOverflow() {
sum().bigDecimalCentsPreservedWithoutOverflow();
}

@Test
void negativeValuesForRefunds() {
sum().negativeValuesForRefunds();
}

@Test
void sumValuesIsolatedByNamespace() {
sum().valuesIsolatedByNamespace();
}

// ---- DistinctStoreScenarios (tumbling) ----

@Test
void applyThenQueryReturnsCardinality() {
distinct().applyThenQueryReturnsCardinality();
}

@Test
void deDupesRepeatedMembers() {
distinct().deDupesRepeatedMembers();
}

@Test
void applyResultReflectsCardinality() {
distinct().applyResultReflectsCardinality();
}

@Test
void distinctValuesIsolatedBySubject() {
distinct().valuesIsolatedBySubject();
}

// ---- PurgeScenarios (tumbling) ----

@Test
void purgeSubjectClearsThatSubjectOnly() {
purge().purgeSubjectClearsThatSubjectOnly();
}

@Test
void purgeNamespaceClearsEntireNamespace() {
purge().purgeNamespaceClearsEntireNamespace();
}

// ---- TumblingScenarios ----

@Test
Expand Down Expand Up @@ -86,4 +206,14 @@ void unsupportedWindowQueryIsDistinguishableFailure() {
void supportedEmptyWindowReturnsKnownZero() {
capability().supportedEmptyWindowReturnsKnownZero();
}

@Test
void unsupportedWindowApplyIsDistinguishableFailure() {
capability().unsupportedWindowApplyIsDistinguishableFailure();
}

@Test
void mixedApplyPartiallyFailsOnUnsupportedWindow() {
capability().mixedApplyPartiallyFailsOnUnsupportedWindow();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@
import com.codeheadsystems.velocity.spi.VelocityBackend;
import com.codeheadsystems.velocity.spi.model.Aggregation;
import com.codeheadsystems.velocity.spi.model.AggregationType;
import com.codeheadsystems.velocity.spi.model.ApplyStatus;
import com.codeheadsystems.velocity.spi.model.BackendCapabilities;
import com.codeheadsystems.velocity.spi.model.FailureCode;
import com.codeheadsystems.velocity.spi.model.Feature;
import com.codeheadsystems.velocity.spi.model.FeatureResult;
import com.codeheadsystems.velocity.spi.model.Intent.CountIntent;
import com.codeheadsystems.velocity.spi.model.PerFeature;
import com.codeheadsystems.velocity.spi.model.QueryContext;
import com.codeheadsystems.velocity.spi.model.Window;
import com.codeheadsystems.velocity.spi.model.WindowType;
Expand All @@ -40,7 +44,7 @@
private final VelocityBackend backend;

/**
* @param backend the backend under test

Check warning on line 47 in velocity-testkit/src/main/java/com/codeheadsystems/velocity/testkit/tck/CapabilityConformanceScenarios.java

View workflow job for this annotation

GitHub Actions / build

no main description
*/
public CapabilityConformanceScenarios(final VelocityBackend backend) {
this.backend = Objects.requireNonNull(backend, "backend");
Expand Down Expand Up @@ -99,6 +103,46 @@
assertValue(queryOne(supported), 0);
}

/**
* Applying to an unsupported window fast-rejects with a distinguishable {@code FAILED} {@link
* PerFeature} carrying {@link FailureCode#UNSUPPORTED_WINDOW} — never a silent {@code APPLIED}
* (the apply-side twin of {@link #unsupportedWindowQueryIsDistinguishableFailure}; ADR 0009 rule
* 1, FR-13). Exercised via the {@link CountStore} path — the guard is aggregation-independent.
*/
public void unsupportedWindowApplyIsDistinguishableFailure() {
final PerFeature pf = applyCount(UNSUPPORTED).get(0);
assertThat(pf.status()).isEqualTo(ApplyStatus.FAILED);
assertThat(pf.result().isSuccess()).isFalse();
assertFailure(pf.result(), FailureCode.UNSUPPORTED_WINDOW);
}

/**
* One apply spanning a supported and an unsupported window applies the supported one and fails
* only the unsupported one — a per-feature partial outcome (FR-34), not all-or-nothing.
*/
public void mixedApplyPartiallyFailsOnUnsupportedWindow() {
final Window supported = backend.capabilities().windows().get(0).window();
final List<PerFeature> results = applyCount(supported, UNSUPPORTED);
assertThat(results).hasSize(2);
final PerFeature applied =
results.stream().filter(pf -> pf.status() == ApplyStatus.APPLIED).findFirst().orElseThrow();
final PerFeature failed =
results.stream().filter(pf -> pf.status() == ApplyStatus.FAILED).findFirst().orElseThrow();
assertThat(applied.result().isSuccess()).isTrue();
assertFailure(failed.result(), FailureCode.UNSUPPORTED_WINDOW);
}

private List<PerFeature> applyCount(final Window... windows) {
if (!(backend instanceof CountStore countStore)) {
throw new IllegalStateException(
"apply-side capability scenarios require a CountStore backend");
}
final Feature feature = Tck.countFeature(windows);
return countStore
.applyCount(Tck.apply(NS_A), List.of(new CountIntent(feature, SUBJECT_A)))
.perFeature();
}

private FeatureResult queryOne(final Window window) {
final QueryContext ctx = Tck.query(NS_A);
if (backend instanceof CountStore countStore) {
Expand Down
Loading
Loading