Skip to content

feat(#115): chunked values — setValueChunked/getValueChunked/deleteValueChunked - #118

Merged
s2x merged 2 commits into
masterfrom
feat/115-chunked-values
Sep 9, 2026
Merged

feat(#115): chunked values — setValueChunked/getValueChunked/deleteValueChunked#118
s2x merged 2 commits into
masterfrom
feat/115-chunked-values

Conversation

@s2x

@s2x s2x commented Sep 9, 2026

Copy link
Copy Markdown
Member

Summary

Implements the chunked-values feature designed in #115 (see the design comment on the issue):

  • Chunk\ChunkKeyCodec (@internal) — the frozen storage format:

    • chunk keys: $key . "\x00" . Tuple::pack([$generation, $index])
    • metadata key: $key . "\x00" . "\x00" (sorts before all chunks; unreachable through the public Tuple/Subspace/Directory API)
    • namespace range: [$key . "\x00", $key . "\x01") — no collision with user text suffixes
    • magic-signed 22-byte metadata record: "FDBCK1" | total_length (8 B) | chunk_count (4 B) | generation (4 B)
  • Transaction::setValueChunked(key, value, chunkSize = 100000) — atomic single-transaction write: namespace clear (stale-tail removal) + chunk writes + metadata record. Capped at MutationBudget::SPLIT_TARGET_BYTES (8,000,000 B) — above the cap ChunkedValueTooLargeException (readonly valueSize/maxSize) is thrown before any mutation.

  • ReadTransaction::getValueChunked(key): ?string — one range read of the active generation; snapshot reads work for free. null for a missing key, "" for an empty value (meta with chunk_count = 0), ChunkedValueCorruptedException on malformed metadata / missing chunks / wrong total length (loud, never garbage).

  • Transaction::deleteValueChunked(key) — one range clear removes meta + all chunks + all generations.

  • Database::setValueChunked(..., atomic: true) (default) — atomic mode via transact(). atomic: false — generation scheme, no size cap: chunk groups (≤ SPLIT_TARGET_BYTES) committed in their own retried transactions, then a final micro-transaction atomically swaps the metadata and clears everything below the new generation (previous generation + orphaned chunks from interrupted attempts). Readers always see either the whole old or the whole new value.

  • Shared @internal MutationBudget accounting from feat(#116): setBatch() helper with mutation-budget enforcement #117; the \x00 codec stays private to the chunked layer (setBatch never touches it).

Closes #115

Testing

  • composer lint — PHPCS + Rector + PHPStan level 9: clean
  • composer test:unit — 610 tests OK, incl. new ChunkKeyCodecTest: namespace ordering, generation range boundaries (incl. the tuple-int encoding size change at 255 → 256), meta record round trip + tamper detection
  • composer test:integration — 308 tests OK, incl. new ChunkedValuesTest (16 tests): round trips (small / empty / multi-chunk > 100 kB / chunk-boundary edge), KeyConvertible keys, snapshot read, overwrite larger→smaller with no stale tail, delete removes everything, atomic cap rejected before mutation + accepted exactly at cap, non-atomic 12 MB write, generation swap cleanup, foreign meta data and missing chunks detected

Docs & changelog

…lueChunked

- Chunk\ChunkKeyCodec (@internal): \x00-namespaced key space — meta key
  (sorts first) + chunk keys Tuple::pack([generation, index]); magic-signed
  22-byte metadata record (FDBCK1 | total_length | chunk_count | generation)
- Transaction::setValueChunked(key, value, chunkSize = 100000): atomic
  single-transaction write (namespace clear + chunks + meta), capped at
  MutationBudget::SPLIT_TARGET_BYTES (8 MB) with ChunkedValueTooLargeException
  (readonly valueSize/maxSize) thrown before any mutation
- ReadTransaction::getValueChunked(): one range read of the active generation;
  null for missing key, '' for empty value, ChunkedValueCorruptedException on
  malformed metadata / missing chunks / wrong total length; snapshot reads
  supported
- Transaction::deleteValueChunked(): one range clear removes meta + all
  chunks + all generations
- Database::setValueChunked(..., atomic = true): atomic mode via transact();
  atomic: false — generation scheme: budget-sized chunk groups across
  multiple transactions, then an atomic meta swap + clear below the new
  generation (self-cleaning orphaned chunks, no size cap)
- Unit tests (ChunkKeyCodecTest): namespace ordering, generation boundaries
  incl. the 255->256 tuple-int encoding size change, meta record round trip
  and tamper detection
- Integration tests (ChunkedValuesTest): round trips, chunk-boundary edge,
  stale-tail overwrite, snapshot read, atomic cap before mutation,
  non-atomic 12 MB write, generation swap cleanup, corruption detection
- docs/chunked-values.md + CHANGELOG entry
Base key 2 bytes below the limit: the metadata key fits exactly at
10,000 bytes but every chunk key (base + separator + packed
[generation, index] suffix) exceeds the limit; set() rejects it before
the transaction queues mutations, nothing is written and the database
stays usable. Closes the last gap versus the literal acceptance
criteria of #115.
@s2x
s2x merged commit c1838c9 into master Sep 9, 2026
6 checks passed
@s2x
s2x deleted the feat/115-chunked-values branch September 9, 2026 16:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Chunked values: setValueChunked() / getValueChunked()

1 participant