Skip to content

Add context map to mutation response payloads#258

Merged
em3s merged 1 commit intomainfrom
refactor/mutation-response-context
Apr 23, 2026
Merged

Add context map to mutation response payloads#258
em3s merged 1 commit intomainfrom
refactor/mutation-response-context

Conversation

@em3s
Copy link
Copy Markdown
Contributor

@em3s em3s commented Apr 22, 2026

Summary

Query payloads (EdgePayload, etc.) already carry a context: Map<String, Any?>? slot; add the same slot to mutation payloads for symmetry. Applies to both V2 and V3 endpoints.

Nullable with @JsonInclude(NON_NULL)non-breaking. Existing callers are unaffected.

Changes

  • core.MutationResult.context: Map<String, Any?>? = null
  • V3 EdgeMutationResponse.Item.context and MultiEdgeMutationResponse.Item.context
  • V2 engine.edge.MutationResultItem.context
  • from(...) factories thread it through unchanged

Tests

  • MutationResponseContextTest (core) — V3 items ser/der: null omitted, non-null round-trip
  • MutationResultItemContextTest (engine) — same for V2 MutationResultItem

@em3s em3s force-pushed the refactor/mutation-response-context branch from f5b5364 to 95ccab3 Compare April 22, 2026 23:51
@em3s em3s changed the title Add context map to mutation response payloads Add context map to V2/V3 mutation response payloads Apr 23, 2026
@em3s em3s changed the title Add context map to V2/V3 mutation response payloads Add context map to mutation response payloads Apr 23, 2026
@em3s em3s force-pushed the refactor/mutation-response-context branch 2 times, most recently from e2a8caf to 95ccab3 Compare April 23, 2026 01:15
Query payloads (`EdgePayload`, `EdgeCountPayload`, `EdgeAggPayload`)
already carry a `context: Map<String, Any?>` slot for per-item
metadata; add the same slot to mutation payloads. Nullable with
`@JsonInclude(NON_NULL)` — existing callers see byte-identical
responses. The key is only emitted once a caller populates it
(see #259, #254).

- `core.MutationResult.context: Map<String, Any?>? = null`
- V3 `EdgeMutationResponse.Item` and `MultiEdgeMutationResponse.Item`:
  nullable + `@field:JsonInclude(Include.NON_NULL)`
- V2 `engine.edge.MutationResultItem`: same
- `from(...)` factories thread `MutationResult.context` through

Tests (ObjectSource-driven, one class per Item type):
- `EdgeMutationResponseItemContextTest` (core) — V3 edge item
- `MultiEdgeMutationResponseItemContextTest` (core) — V3 multi-edge item
- `MutationResultItemContextTest` (engine) — V2 mutation result item

Each class has two parameterized methods (`Item serializes`,
`Item deserializes`) whose YAML cases list input and expected
JSON/context — makes intent readable from the test data alone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s force-pushed the refactor/mutation-response-context branch from 95ccab3 to f3389bc Compare April 23, 2026 01:43
@em3s em3s marked this pull request as ready for review April 23, 2026 03:05
@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Apr 23, 2026
@em3s
Copy link
Copy Markdown
Contributor Author

em3s commented Apr 23, 2026

Merging

@dosubot dosubot Bot added the enhancement New feature or request label Apr 23, 2026
@em3s em3s merged commit 3692c41 into main Apr 23, 2026
8 checks passed
em3s added a commit that referenced this pull request Apr 23, 2026
Add an opt-in header (`AB-Include-Mutation-Context: true`,
case-insensitive) that enriches mutation responses with an empty
`context` map. Without the header, `context` stays `null` and is
omitted from JSON — wire-identical for existing callers.

End-to-end tests drive the full request → response path to confirm
#258 is non-breaking on the wire.

- New `mapToResponseEntity(exchange: ServerWebExchange)` overload on
  `Mono<T>` that reads the header and enriches V2 `MutationResult`,
  V3 `EdgeMutationResponse`, and V3 `MultiEdgeMutationResponse` items
- V3 `EdgeMutationController` / `MultiEdgeMutationController` now
  accept `ServerWebExchange` and route through the new overload
- `ResponseEntityExtensionsTest` (unit) — V2 + V3 enrichment,
  case-insensitive header, pass-through for non-mutation bodies
- `MutationContextOptInE2ETest` (e2e via `WebTestClient`) — V3 sync
  endpoints, header on/off

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Add an opt-in header (`AB-Include-Mutation-Context: true`,
case-insensitive, parsed as Boolean) that flows from V2/V3 mutation
controllers into the service layer. When set, the service attaches an
empty `context` map to each response item; otherwise `context` stays
`null` and is omitted from JSON — wire-identical for existing callers.

End-to-end tests drive both V3 and V2 paths to confirm #258 is
non-breaking on the wire.

Server stays thin: controllers receive the header as a typed Boolean
and pass it through. The enrichment itself happens in `MutationService`
(V3) and `Graph` (V2), right where `MutationResult` is assembled.

- V3 `MutationService.mutate(..., includeContext)` — flag threads
  through `.collectList()` and maps `it.copy(context = emptyMap())`
  when set
- V2 `Graph.mutate(...)` and its `upsert` / `update` / `delete` /
  id-variant overloads — same pattern
- V3 controllers (`EdgeMutationController`, `MultiEdgeMutationController`)
  and V2 `EdgeController` receive `@RequestHeader(INCLUDE_MUTATION_CONTEXT_HEADER, required = false, defaultValue = "false") includeContext: Boolean`
- Other V2 service-label controllers (`ServiceLabelEdge*`) keep the
  default `false` via `Graph`'s default parameter — no call-site changes
- `ResponseEntityExtensions.mapToResponseEntity()` restored to its
  original one-liner; only `INCLUDE_MUTATION_CONTEXT_HEADER` const
  remains for controllers to reference
- `ResponseEntityExtensionsTest` removed — the enrichment logic now
  lives in engine code and is exercised through the e2e test
- `MutationContextOptInE2ETest` covers V3 edge/multi-edge sync and
  V2 edge endpoints, header on/off

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Add an opt-in header `AB-Include-Mutation-Context` (case-insensitive,
parsed as Boolean) that attaches an empty `context` map to each
mutation response item. Without it, `context` stays `null` and is
omitted from JSON — wire-identical for existing callers.

End-to-end tests cover both V3 and V2 paths to confirm #258 is
non-breaking on the wire.

V3 stays zero-touch on controllers: the flag rides on `RequestContext`,
which the existing argument resolver already assembles from headers.
`MutationService` checks `requestContext.includeContext` when
materialising `MutationResult`s.

V2 `Graph.upsert/update/delete` gain an `includeContext: Boolean = false`
parameter that threads into `Graph.mutate`. V2 `EdgeController` reads
the header and forwards it; other V2 service-label controllers pick up
the default (`false`) without any call-site change.

- `HttpHeaderConstants.INCLUDE_MUTATION_CONTEXT` — new header name
- `engine.context.RequestContext.includeContext` — new field, default `false`
- `ServerRequestContextArgumentResolver` — reads the header
- `engine.service.MutationService.mutate` — reads flag from `requestContext`
- `v2.engine.Graph.mutate` + `upsert`/`update`/`delete` overloads —
  `includeContext` parameter, applies `emptyMap()` copy on the list
- V2 `EdgeController` — `@RequestHeader(... INCLUDE_MUTATION_CONTEXT, defaultValue = "false")`
- `MutationContextOptInE2ETest` — V3 edge/multi-edge sync + V2 edge,
  header on/off, case-insensitive

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Add an opt-in header `AB-Include-Mutation-Context` (case-insensitive,
parsed as Boolean) that attaches an empty `context` map to each
mutation response item. Without it, `context` stays `null` and is
omitted from JSON — wire-identical for existing callers.

End-to-end tests cover both V3 and V2 paths to confirm #258 is
non-breaking on the wire.

V3 stays zero-touch on controllers: the flag rides on `RequestContext`,
which the existing argument resolver already assembles from headers.
`MutationService` checks `requestContext.includeContext` when
materialising `MutationResult`s.

V2 `Graph.upsert/update/delete` gain an `includeContext: Boolean = false`
parameter that threads into `Graph.mutate`. V2 `EdgeController` reads
the header and forwards it; other V2 service-label controllers pick up
the default (`false`) without any call-site change.

- `HttpHeaderConstants.INCLUDE_MUTATION_CONTEXT` — new header name
- `engine.context.RequestContext.includeContext` — new field, default `false`
- `ServerRequestContextArgumentResolver` — reads the header
- `engine.service.MutationService.mutate` — reads flag from `requestContext`
- `v2.engine.Graph.mutate` + `upsert`/`update`/`delete` overloads —
  `includeContext` parameter, applies `emptyMap()` copy on the list
- V2 `EdgeController` — `@RequestHeader(... INCLUDE_MUTATION_CONTEXT, defaultValue = "false")`
- `MutationContextOptInE2ETest` — V3 edge/multi-edge sync + V2 edge,
  header on/off, case-insensitive

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Add an opt-in header `AB-Include-Mutation-Context` (case-insensitive,
parsed as Boolean) that attaches an empty `context` map to each
mutation response item. Without it, `context` stays `null` and is
omitted from JSON — wire-identical for existing callers.

End-to-end tests cover both V3 and V2 paths to confirm #258 is
non-breaking on the wire.

V3 stays zero-touch on controllers: the flag rides on `RequestContext`,
which the existing argument resolver already assembles from headers.
`MutationService` checks `requestContext.includeContext` when
materialising `MutationResult`s.

V2 `Graph.upsert/update/delete` gain an `includeContext: Boolean = false`
parameter that threads into `Graph.mutate`. V2 `EdgeController` reads
the header and forwards it; other V2 service-label controllers pick up
the default (`false`) without any call-site change.

- `HttpHeaderConstants.INCLUDE_MUTATION_CONTEXT` — new header name
- `engine.context.RequestContext.includeContext` — new field, default `false`
- `ServerRequestContextArgumentResolver` — reads the header
- `engine.service.MutationService.mutate` — reads flag from `requestContext`
- `v2.engine.Graph.mutate` + `upsert`/`update`/`delete` overloads —
  `includeContext` parameter, applies `emptyMap()` copy on the list
- V2 `EdgeController` — `@RequestHeader(... INCLUDE_MUTATION_CONTEXT, defaultValue = "false")`
- `MutationContextOptInE2ETest` — V3 edge/multi-edge sync + V2 edge,
  header on/off, case-insensitive

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258 and #259. When the caller sends
`AB-Include-Mutation-Context`, mutation responses include the
Put/Delete/Increment operations issued to HBase under
`context["storage_ops"]`, so shadow tests can compare at the operation
level.

- `core.storage.StorageOp` — sealed hierarchy for hex-encoded ops
- `engine.storage.StorageOpCollector` — thread-safe appender with a
  per-RMW cap so an opt-in request can't produce an unbounded payload
- Collector allocated only when `RequestContext.includeContext` is
  true, threaded through `MutationContext` and
  `TableBinding.write(..., collector)`
- V3 `MutationService.mutate` — passes collector into
  `readModifyWrite`, attaches `context = mapOf("storage_ops" to ops)`
  on completion
- V2 `Graph.mutate` — same pattern via `AbstractLabel` and
  `HBaseHashLabel.handleDeferredRequests`
- `HBaseOpConverter` — HBase `Put` / `Delete` / `Increment` → `StorageOp`
- `CdcContext.storageOps` is `@JsonIgnore` so captured ops never leak
  into CDC / Kafka payloads

Tests:
- `HBaseOpConverterTest` — conversion round-trip
- `CdcContextSerializationTest` — CDC payload excludes `storageOps`
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e,
  header on/off
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e; service-scoped
  V2 controllers keep the default and are out of scope

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258 and #259. When the caller sends
`AB-Include-Mutation-Context`, mutation responses include the
Put/Delete/Increment operations issued to HBase under
`context["storage_ops"]`, so shadow tests can compare at the operation
level.

- `core.storage.StorageOp` — sealed hierarchy for hex-encoded ops
- `engine.storage.StorageOpCollector` — thread-safe appender with a
  per-RMW cap so an opt-in request can't produce an unbounded payload
- V3 (`MutationService`): the opt-in check happens once at the top;
  a per-RMW `StorageOpCollector?` rides the rest of the chain.
  `readModifyWrite(..., collector)` has no Boolean flag — null means
  don't capture, non-null means do.
- V2 (`Graph` / `AbstractLabel` / `NilLabel` / `LocalBackedJdbcHashLabel`):
  `mutate`/`upsert`/`update`/`delete` take a
  `collectorFactory: (() -> StorageOpCollector)? = null`. `AbstractLabel`
  invokes the factory per edge, giving each `MutationResultItem` its own
  `storage_ops`. No Boolean `includeContext` parameter anywhere.
- V2 `EdgeController` builds the factory from
  `RequestContext.includeContext` once and forwards it.
- `HBaseOpConverter` — HBase `Put` / `Delete` / `Increment` → `StorageOp`
- `CdcContext.storageOps` is `@JsonIgnore` so captured ops never leak
  into CDC / Kafka payloads

Tests:
- `HBaseOpConverterTest` — conversion round-trip
- `CdcContextSerializationTest` — CDC payload excludes `storageOps`
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e, header on/off
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e; service-scoped
  V2 controllers keep the default and are out of scope

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Follows #258 and #259. Introduces the request-scoped collector mechanism
that will surface storage-level operations on mutation responses (see
#253). This PR lands the API surface and the plumbing only —
`StorageOpCollector.collect` is a no-op scaffold, so header-on requests
emit `context.storage_ops = []` but no actual ops are captured yet.

- `core.storage.StorageOp` — sealed hierarchy (`Put`/`Delete`/`Increment`
  with hex cells/deltas)
- `engine.storage.StorageOpCollector` — thread-safe appender with a
  per-RMW cap. Backend-agnostic: `collect(request, table)` is a no-op;
  interpretation lands in the follow-up
- V3 `MutationService.readModifyWrite` — takes
  `collector: StorageOpCollector?`; `MutationService.mutate` gates on
  `RequestContext.includeContext`, creates a per-RMW collector, and
  attaches `context = mapOf("storage_ops" to ops)`
- V2 `Graph.mutate` + `upsert`/`update`/`delete` (and id-variants) —
  take `collectorFactory: (() -> StorageOpCollector)? = null`;
  `AbstractLabel` invokes the factory per edge so each
  `MutationResultItem` gets its own `storage_ops`
- V2 `Label` / `NilLabel` / `LocalBackedJdbcHashLabel` — signature
  updates for the factory
- V2 `EdgeController` — `RequestContext.collectorFactory()` extension
  builds the factory from the header
- `HBaseHashLabel.handleDeferredRequests` — one line:
  `collector?.collectAll(deferredRequests, t.edge.name.nameAsString)`
- `CdcContext.storageOps` — nullable + `@JsonIgnore` so the field
  exists in the pipeline but CDC/Kafka payloads never carry it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Extends the scaffolded
`StorageOp` marker with the backend-facing shape and teaches
`StorageOpCollector.collect` to interpret raw HBase requests. No
changes to call sites — the plumbing was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation
responses include the captured ops under `context["storage_ops"]`.

- `StorageOp` — adds `Put`/`Delete`/`Increment` data classes with
  `Cell`/`Delta` nested types, hex-encoded row/family/qualifier/value
- `StorageOpCollector.collect(request, table)` — `when` on
  `Put`/`Delete`/`Increment`, extracts cells/deltas, hex-encodes
  row/family/qualifier/value. Unknown request types are silently
  dropped so new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + two narrow
  `@Test` cases (non-Mutation skip, truncation cap)
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with
  header on, asserts `context.storage_ops[0].rowHex` shape
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Implements
`StorageOpCollector.collect` so it decodes raw HBase requests into the
`StorageOp` sealed type. The scaffolded marker is extended with the
backend-facing shape: `Put` / `Delete` / `Increment` data classes with
`Cell` / `Delta` nested types, base64-encoded
row / family / qualifier / value. No call-site changes — the plumbing
was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation responses
include the captured ops under `context["storage_ops"]`.

Unknown request types become `StorageOp.Unknown(type = class name)` so
new backends can join without touching call sites.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put/Delete/Delete-columns/Increment/multi-byte) + `@Test` for
  non-Mutation fallthrough and truncation cap
- `CdcContextSerializationTest` — `storageOps` excluded from CDC
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with header
  on, asserts `context.storage_ops[0].row` / cells shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@em3s em3s self-assigned this Apr 23, 2026
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Implements
`StorageOpCollector.collect` so it decodes raw HBase requests into the
`StorageOp` sealed type. The scaffolded marker is extended with the
backend-facing shape: `Put` / `Delete` / `Increment` data classes with
`Cell` / `Delta` nested types, base64-encoded
row / family / qualifier / value. No call-site changes — the plumbing
was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation responses
include the captured ops under `context["storageOps"]`. When more than
`DEFAULT_MAX_OPS` (1024) ops would be collected, the collector sets
`context["storageOpsTruncated"] = true` so clients can tell a complete
snapshot from a silently truncated one.

`StorageOp.Cell.type` captures the HBase `Cell.Type` name (`Put`,
`DeleteColumn`, `DeleteFamily`, `DeleteFamilyVersion`, `Delete`) so
shadow tests can distinguish delete subtypes.

Unknown request types become `StorageOp.Unknown(type = qualified class
name)` with a one-shot `warn`-level log per type, so new backends can
join without silent drops.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put / Delete-full-row / Delete-columns / Delete-family / Increment /
  multi-byte) + `@Test` for multi-CF Put, non-Mutation fallthrough,
  `Append` → `Unknown`, truncation cap, `toContextMap` flag
- `CdcContextSerializationTest` — `storageOps` and
  `storageOpsTruncated` excluded from CDC
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with header
  on, asserts `context.storageOps[0].row` / cells shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Implements
`StorageOpCollector.collect` so it decodes raw HBase requests into the
`StorageOp` sealed type. The scaffolded marker is extended with the
backend-facing shape: `Put` / `Delete` / `Increment` data classes with
`Cell` / `Delta` nested types, base64-encoded
row / family / qualifier / value. No call-site changes — the plumbing
was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation responses
include the captured ops under `context["storageOps"]`. When more than
`DEFAULT_MAX_OPS` (1024) ops would be collected, the collector sets
`context["storageOpsTruncated"] = true` so clients can tell a complete
snapshot from a silently truncated one.

`StorageOp.Cell.type` captures the HBase `Cell.Type` name (`Put`,
`DeleteColumn`, `DeleteFamily`, `DeleteFamilyVersion`, `Delete`) so
shadow tests can distinguish delete subtypes.

Unknown request types become `StorageOp.Unknown(type = qualified class
name)` with a one-shot `warn`-level log per type, so new backends can
join without silent drops.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put / Delete-full-row / Delete-columns / Delete-family / Increment /
  multi-byte) + `@Test` for multi-CF Put, non-Mutation fallthrough,
  `Append` → `Unknown`, truncation cap, `toContextMap` flag
- `CdcContextSerializationTest` — `storageOps` and
  `storageOpsTruncated` excluded from CDC
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with header
  on, asserts `context.storageOps[0].row` / cells shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Closes #253. Follows #258, #259, #261. Implements
`StorageOpCollector.collect` so it decodes raw HBase requests into the
`StorageOp` sealed type. The scaffolded marker is extended with the
backend-facing shape: `Put` / `Delete` / `Increment` data classes with
`Cell` / `Delta` nested types, base64-encoded
row / family / qualifier / value. No call-site changes — the plumbing
was done in #261.

When the `AB-Include-Mutation-Context` header is set, mutation responses
include the captured ops under `context["storageOps"]`. When more than
`DEFAULT_MAX_OPS` (1024) ops would be collected, the collector sets
`context["storageOpsTruncated"] = true` so clients can tell a complete
snapshot from a silently truncated one.

`StorageOp.Cell.type` captures the HBase `Cell.Type` name (`Put`,
`DeleteColumn`, `DeleteFamily`, `DeleteFamilyVersion`, `Delete`) so
shadow tests can distinguish delete subtypes.

Unknown request types become `StorageOp.Unknown(type = qualified class
name)` with a one-shot `warn`-level log per type, so new backends can
join without silent drops.

Tests:
- `StorageOpCollectorTest` — data-driven via `@ObjectSource`
  (Put / Delete-full-row / Delete-columns / Delete-family / Increment /
  multi-byte) + `@Test` for multi-CF Put, non-Mutation fallthrough,
  `Append` → `Unknown`, truncation cap, `toContextMap` flag
- `CdcContextSerializationTest` — `storageOps` and
  `storageOpsTruncated` excluded from CDC
- `V2EdgeStorageOpsSpec` — V2 `/graph/v2/edge` e2e with header on
- `EdgeMutationStorageOpsSpec` — V3 edge + multi-edge e2e with header
  on, asserts `context.storageOps[0].row` / cells shape

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
em3s added a commit that referenced this pull request Apr 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:S This PR changes 10-29 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant