Skip to content

feat: support zero-key (empty) indexes#12

Merged
tamirms merged 2 commits into
mainfrom
feat/empty-index-support
Jul 13, 2026
Merged

feat: support zero-key (empty) indexes#12
tamirms merged 2 commits into
mainfrom
feat/empty-index-support

Conversation

@tamirms

@tamirms tamirms commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Summary

NewSortedBuilder and NewUnsortedBuilder now accept totalKeys == 0 and produce a valid empty index whose every QueryRank returns ErrNotFound, instead of failing with ErrEmptyIndex. A caller with no keys no longer needs a special case.

Motivation

A downstream cold-index build over a zero-key window (stellar/stellar-rpc#826) returned ErrEmptyIndex, so the caller had to special-case the empty result. When a coverage window contained only empty chunks, that special case stalled the daemon. Treating a zero-key index as a first-class empty result lets callers build unconditionally.

What changed

The on-disk format already tolerated this: the header permits a zero-key index, and the query path returns ErrNotFound for every key when no block holds a key. The only blockers were an artificial guard and one lazy-init hazard.

  • Removed the totalKeys == 0 guard in newBuilder. numBlocks() floors at 2, so a zero-key build flows through the existing empty-block machinery (commitEmptyBlock) and finalizes two empty blocks; every lookup misses.
  • Forced single-threaded for a zero-key build, since there is nothing to parallelize, so the empty case takes the simple finish path instead of spinning up an unused parallel writer pipeline.
  • The unsorted Finish emits the empty index directly when its lazily-created buffer was never initialized (no keys added), because finishUnsorted dereferences that buffer immediately and would otherwise panic.
  • Extracted commitTrailingEmptyBlocksAndFinalize, shared by the sorted single-threaded finish and the unsorted zero-key path.
  • Removed ErrEmptyIndex, which is no longer returned. This is a clean break, appropriate pre-1.0.

Testing

  • New TestEmptyIndex: both algorithms, both builder types, and mphf-only versus the payload-plus-fingerprint layout, asserting the build succeeds, the index opens, passes integrity Verify(), reports zero keys, and every QueryRank and QueryPayload returns ErrNotFound.
  • TestEmptyIndexWithWorkers pins that a zero-key build requested with WithWorkers stays off the parallel finish paths.
  • Rewrote TestBuilderZeroKeys for the new contract.
  • The full existing suite, go vet, golangci-lint, and the correctness, corrupt-input, and parallel-pipeline fuzzers all pass.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings July 13, 2026 12:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes the builder contract to treat totalKeys == 0 as a valid “empty index” build result (instead of returning ErrEmptyIndex), ensuring empty windows can be built without caller-side special casing while preserving query semantics (QueryRank/QueryPayload miss with ErrNotFound).

Changes:

  • Allow NewSortedBuilder/NewUnsortedBuilder to accept totalKeys == 0, forcing single-threaded completion for that case.
  • Prevent an unsorted-build nil dereference by emitting the empty index directly when no keys were ever added.
  • Remove ErrEmptyIndex and add/adjust tests to pin the new empty-index behavior across algorithms/build modes.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test_helpers_test.go Removes helper-level “empty input” guards so tests can build empty indexes.
sentinels.go Removes exported ErrEmptyIndex sentinel from the public API surface.
internal/sherr/errors.go Deletes the internal ErrEmptyIndex definition.
error_test.go Updates TestBuilderZeroKeys to assert empty-index success + ErrNotFound query behavior.
empty_index_test.go Adds comprehensive empty-index coverage across algorithms, builder types, and payload layouts (plus WithWorkers).
builder.go Removes the totalKeys==0 guard, forces single-threaded empty builds, and extracts shared empty-block finalize helper.
builder_unsorted.go Adds the zero-key unsorted Finish() fast path to avoid nil unsortedBuf dereference.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread sentinels.go
Remove the newBuilder guard that rejected totalKeys == 0. numBlocks()
floors at 2, so a zero-key build now flows through the existing
empty-block machinery and produces a valid index whose every QueryRank
returns ErrNotFound. Force single-threaded for a zero-key build (nothing
to parallelize) and guard the unsorted Finish, whose lazily-created
buffer is nil when no keys were ever added.

Remove ErrEmptyIndex: zero keys is no longer an error.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tamirms tamirms force-pushed the feat/empty-index-support branch from da9a38b to 9f6eeb7 Compare July 13, 2026 14:11
tamirms added a commit to stellar/stellar-rpc that referenced this pull request Jul 13, 2026
…826)

streamhash now accepts zero-key builds (stellar/streamhash#12), so the cold
stores no longer special-case an empty coverage.

txhash (the #826 stall): BuildColdIndex drops both ErrEmptyBuildSet returns and
builds a valid empty index for a zero-key coverage (no inputs, or inputs with no
entries — a window of only zero-transaction chunks). buildTxhashIndex then
commits normally, so an all-empty window freezes instead of re-planning every
restart. Removes the #826 TODO at resolveWindow.

events: drop the zero-length index.hash sentinel. WriteColdIndex builds a real
streamhash empty index over zero terms (deleting writeEmptyColdIndex); openMPHF
always opens it; isEmpty is NumKeys()==0. Both stores now handle empty
identically.

go.mod points streamhash at the unmerged PR #12 commit for development; re-pin to
a merged commit/tag before landing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the ErrEmptyIndex row from the §7.5 error reference (the error was
removed) and note that N == 0 builds a valid empty index whose queries
return ErrNotFound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread builder.go
Comment thread builder_unsorted.go
Comment thread streamhash-spec.md
@tamirms tamirms enabled auto-merge (squash) July 13, 2026 16:46
@tamirms tamirms merged commit c72a4e6 into main Jul 13, 2026
12 checks passed
@tamirms tamirms deleted the feat/empty-index-support branch July 13, 2026 16:46
tamirms added a commit to stellar/stellar-rpc that referenced this pull request Jul 13, 2026
…826)

streamhash now accepts zero-key builds (stellar/streamhash#12), so the cold
stores no longer special-case an empty coverage.

txhash (the #826 stall): BuildColdIndex drops both ErrEmptyBuildSet returns and
builds a valid empty index for a zero-key coverage (no inputs, or inputs with no
entries — a window of only zero-transaction chunks). buildTxhashIndex then
commits normally, so an all-empty window freezes instead of re-planning every
restart. Removes the #826 TODO at resolveWindow.

events: drop the zero-length index.hash sentinel. WriteColdIndex builds a real
streamhash empty index over zero terms (deleting writeEmptyColdIndex); openMPHF
always opens it; isEmpty is NumKeys()==0. Both stores now handle empty
identically.

go.mod points streamhash at the unmerged PR #12 commit for development; re-pin to
a merged commit/tag before landing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

4 participants