Skip to content

Allow FullMergeV3 to produce a deletion result via std::monostate#14607

Open
zaidoon1 wants to merge 1 commit into
facebook:mainfrom
zaidoon1:merge-operator-deletion-result
Open

Allow FullMergeV3 to produce a deletion result via std::monostate#14607
zaidoon1 wants to merge 1 commit into
facebook:mainfrom
zaidoon1:merge-operator-deletion-result

Conversation

@zaidoon1

Copy link
Copy Markdown
Contributor

Add std::monostate as a fourth alternative to
MergeOperationOutputV3::NewValue. When a merge operator sets this variant, the key is treated as deleted:

  • Get()/MultiGet() return Status::NotFound()
  • Iterators skip the key (forward and backward)
  • Compaction produces a deletion tombstone, or drops the key entirely when the full history has been seen at the bottommost level

This enables use cases like counter auto-delete-at-zero, conditional mutations, and Cassandra-style row expiration without requiring a separate compaction filter.

Closes #14593

@meta-cla meta-cla Bot added the CLA Signed label Apr 12, 2026
@github-actions

github-actions Bot commented Apr 12, 2026

Copy link
Copy Markdown

⚠️ clang-tidy: 3 warning(s) on changed lines

Completed in 611.1s.

Summary by check

Check Count
concurrency-mt-unsafe 2
performance-inefficient-vector-operation 1
Total 3

Details

db/db_merge_operator_test.cc (1 warning(s))
db/db_merge_operator_test.cc:2854:5: warning: 'emplace_back' is called inside a loop; consider pre-allocating the container capacity before the loop [performance-inefficient-vector-operation]
db_stress_tool/db_stress_tool.cc (2 warning(s))
db_stress_tool/db_stress_tool.cc:259:7: warning: function is not thread safe [concurrency-mt-unsafe]
db_stress_tool/db_stress_tool.cc:266:7: warning: function is not thread safe [concurrency-mt-unsafe]
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch 5 times, most recently from 30c844c to f66c937 Compare April 12, 2026 08:17
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch 3 times, most recently from 988e992 to 7ecc3ef Compare May 26, 2026 09:29
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Codex Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

🟡 Codex Code Review

Auto-triggered after CI passed — reviewing commit 7ecc3ef


Codex review failed before producing findings.

WARNING: proceeding, even though we could not update PATH: Refusing to create helper binaries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/codex-home"))
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'

Usage: codex exec review --commit <SHA> --base <BRANCH> --title <TITLE> --model <MODEL> --config <key=value> --dangerously-bypass-approvals-and-sandbox --output-last-message <FILE> [PROMPT]

For more information, try '--help'.

ℹ️ About this response

Generated by Codex CLI.
Review methodology: claude_md/code_review.md

Limitations:

  • Codex may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /codex-review [context] — Request a code review
  • /codex-query <question> — Ask about the PR or codebase
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Claude Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit 7ecc3ef


Summary

Well-structured PR that adds a fourth std::monostate alternative to MergeOperationOutputV3::NewValue, enabling merge operators to signal key deletion. The design is sound -- restricting deletion to FullMergeV3, materializing as standard kTypeDeletion for on-disk compatibility, and including ~1800 lines of comprehensive tests. However, a critical bug in an unmodified file (GetContext::PostprocessMerge) will cause merge-deletion to be misreported as database corruption on point lookups that complete within a single level.

High-severity findings (1):

  • [table/get_context.cc:679] PostprocessMerge() treats merge-deletion Status::NotFound as corruption, breaking point lookups when merge completes inside GetContext.
Full review (click to expand)

Findings

🔴 HIGH

H1. GetContext::PostprocessMerge mishandles merge-deletion as corruption -- table/get_context.cc:679-693
  • Issue: The point-lookup variant of TimedFullMergeImpl returns Status::NotFound(SubCode::kMergeOperatorDeletion) when the merge operator produces std::monostate. GetContext::PostprocessMerge() only recognizes kMergeOperatorFailed; any other non-OK status (including the new NotFound) falls through to state_ = kCorrupt. This means a single-level point lookup (where all merge operands and base value are in the same SST or memtable) on a merge-deleted key will return Status::Corruption instead of Status::NotFound.
  • Root cause: table/get_context.cc is not in the PR's changed file list. The existing code:
    Status GetContext::PostprocessMerge(const Status& merge_status) {
      if (!merge_status.ok()) {
        if (merge_status.subcode() == Status::SubCode::kMergeOperatorFailed) {
          state_ = kMergeOperatorFailed;
        } else {
          state_ = kCorrupt;  // <-- merge-deletion NotFound lands here
        }
        return merge_status;
      }
      // ...
    }
  • Impact: Any Get() / MultiGet() where merge resolution happens inside GetContext (common when all data is in one level or memtable) will report corruption instead of NotFound. This affects the memtable merge-completion paths at memtable.h:451, memtable.h:478, and memtable.h:520 (where the PR adds comments saying "TimedFullMerge returns Status::NotFound(), which is the correct result for point lookups" -- but the downstream consumer in GetContext doesn't agree).
  • Suggested fix: Add handling for kMergeOperatorDeletion:
    if (merge_status.IsNotFound() &&
        merge_status.subcode() == Status::SubCode::kMergeOperatorDeletion) {
      state_ = kNotFound;
      return merge_status;
    }

🟡 MEDIUM

M1. Missing src.mk and Makefile entries for counter_delete.cc
  • Issue: The diff adds utilities/merge_operators/counter_delete.cc to BUCK and CMakeLists.txt, but src.mk and Makefile do not appear to be updated. Per CLAUDE.md: "When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK." (Diff was truncated -- please verify.)
M2. FindValueForCurrentKeyUsingSeek assert may be overly strong -- db/db_iter.cc:1580
  • Issue: The PR replaces valid_ = true with assert(status_.ok()). If MergeWithBlobBaseValue encounters an I/O error during blob fetch, status_ might not be OK when reaching this assert.
  • Suggested fix: Verify all MergeWith* callers return false on failure before reaching this point, or weaken to assert(status_.ok() || !valid_).
M3. Compaction iterator implicit handling of empty merge results
  • Issue: When MergeUntil produces merge-deletion at bottom level, the compaction iterator reuses the "filtered operands" code path (setting has_current_user_key_ = false). This works but is undocumented.
  • Suggested fix: Add a comment in the compaction iterator noting empty merge results can arise from merge-operator deletion.

🟢 LOW / NIT

L1. Test merge operators defined inline (~120 lines in test file) -- acceptable for test-only code.

Cross-Component Analysis

Context Safe? Action?
WritePreparedTxnDB YES None
ReadOnly DB YES None
User-defined timestamps YES None
BlobDB YES (tested) None
Prefix seek YES (tested) None
Old snapshots YES (tested) None
GetContext single-level merge NO Fix required (H1)

Positive Observations

  1. Comprehensive test coverage: ~1800 lines covering iteration, compaction, snapshots, MultiGet, WBWI, transactions, range tombstones, multi-CF, blob storage, wide columns, and stress testing.
  2. Sound on-disk format: Standard kTypeDeletion entries -- backward compatible with older RocksDB.
  3. Correct variant ordering with static_assert: Enforces std::string first, std::monostate last.
  4. Careful iterator lifecycle: ReleaseTempPinnedData() and removal of redundant valid_ = true show attention to detail.
  5. Bottommost compaction: Skip-older-entries loop correctly prevents stale versions from resurfacing.

ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/code_review.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch 4 times, most recently from a673fcf to e18bb9b Compare May 26, 2026 16:14
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Codex Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

🟡 Codex Code Review

Auto-triggered after CI passed — reviewing commit e18bb9b


Codex review failed before producing findings.

WARNING: proceeding, even though we could not update PATH: Refusing to create helper binaries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/codex-home"))
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'

Usage: codex exec review --commit <SHA> --base <BRANCH> --title <TITLE> --model <MODEL> --config <key=value> --dangerously-bypass-approvals-and-sandbox --output-last-message <FILE> [PROMPT]

For more information, try '--help'.

ℹ️ About this response

Generated by Codex CLI.
Review methodology: claude_md/code_review.md

Limitations:

  • Codex may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /codex-review [context] — Request a code review
  • /codex-query <question> — Ask about the PR or codebase
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Claude Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit e18bb9b


Summary

Well-designed feature that adds std::monostate as a fourth alternative to MergeOperationOutputV3::NewValue, enabling merge operators to signal key deletion. The implementation covers Get, MultiGet, forward/reverse iteration, compaction (both bottommost and non-bottommost), merge-on-write (max_successive_merges), WAL recovery, and snapshots. Test coverage is extensive (~1870 lines) with good cross-feature tests (WBWI, transactions, range tombstones, blob storage, wide columns, prefix bloom). The diff was truncated (28 files changed), so changes to merge_operator.h, status.h, get_context.cc, and potentially WBWI code are not visible but are inferred to exist from the test assertions.

High-severity findings (0):

No high-severity findings.

Full review (click to expand)

Findings

🔴 HIGH

No high-severity findings. All initially flagged critical issues were reconciled against the diff and found to be addressed (see reconciliation notes below).

🟡 MEDIUM

M1. Breaking API Change -- include/rocksdb/merge_operator.h
  • Issue: Adding std::monostate to MergeOperationOutputV3::NewValue (changing from std::variant<std::string, NewColumns, Slice> to std::variant<std::string, NewColumns, Slice, std::monostate>) is a hard breaking change for anyone using std::visit on the variant. Existing code with only 3 visitor lambdas will fail to compile.
  • Root cause: Variant type expansion is inherently breaking for exhaustive visitors.
  • Suggested fix: Document the breaking change in release notes. Consider adding a utility visitor helper that provides a default no-op monostate handler. Since MergeOperationOutputV3 is relatively new (V3 API), the impact surface is limited. The variant.index() values for existing alternatives (0=string, 1=NewColumns, 2=Slice) are preserved since monostate is appended at position 3.
M2. Dual-Overload Return Pattern Inconsistency -- db/merge_helper.cc
  • Issue: The two TimedFullMergeImpl overloads use different return patterns for monostate:
    • Overload 1 (used by DBIter, compaction, write batch): Returns Status::OK() + *result_type = kTypeDeletion
    • Overload 2 (used by GetContext, WBWI): Returns Status::NotFound(kMergeOperatorDeletion)
  • Root cause: Overload 2 lacks a result_type output parameter, so Status is overloaded to signal deletion.
  • Suggested fix: This is a deliberate design choice, but it increases cognitive load. Consider adding a brief comment at the top of each overload explaining the contract, e.g., "This overload signals monostate via Status::NotFound; callers must check IsNotFound before treating as error." Alternatively, consider adding a ValueType* parameter to overload 2 for consistency.
M3. Test Coverage Gaps
  • Issue: The extensive test suite (~47 tests) misses several scenarios:
    1. User-defined timestamps with merge deletion
    2. PartialMerge interaction -- what happens when PartialMerge is called on operands before FullMergeV3 resolves to monostate?
    3. GetMergeOperands API -- does DB::GetMergeOperands() handle the deletion case?
    4. Tailing iterator with merge deletion
    5. ReadOnly DB / Secondary instance with merge-deleted keys
    6. IngestExternalFile with merge deletion across ingested boundaries
  • Suggested fix: Add targeted tests for at least items 1-3. Items 4-6 are lower priority.
M4. WBWI / Transaction Handling Uncertain -- utilities/write_batch_with_index/
  • Issue: The diff is truncated and WBWI source changes are not visible. However, tests assert correct behavior (MergeDeletionWBWIGetFromBatchDeletes, MergeDeletionWBWIGetFromBatchAndDB, MergeDeletionWBWIBaseDeltaIterator, MergeDeletionTransactionGet). If WBWI code is NOT modified to handle Status::NotFound(kMergeOperatorDeletion) from the second TimedFullMerge overload, these tests would fail. The WriteBatchWithIndexInternal merge code currently expects merges to succeed with Status::OK() or fail with corruption.
  • Suggested fix: Confirm that WBWI merge resolution code (in write_batch_with_index_internal.cc) properly handles the new NotFound status by translating it to the WBWIIteratorImpl::kDeleted result state. The tests provide good coverage if the code is correct.

🟢 LOW / NIT

L1. Documentation Update for MergeOperationOutputV3 -- include/rocksdb/merge_operator.h:189-196
  • Issue: The comment says "Can be one of three things" but should say "four" after adding monostate. The NewValue type alias comment should document std::monostate (deletion).
  • Suggested fix: Update comment to describe all four alternatives.
L2. WAL Recovery Determinism Requirement
  • Issue: The WAL records the original kTypeMerge, not the synthesized kTypeDeletion. On recovery, the merge operator must deterministically reproduce the same deletion result. This is consistent with the existing merge operator contract but is not explicitly documented for the monostate case.
  • Suggested fix: Add a note in the FullMergeV3 documentation: "If the merge operator returns std::monostate, it must do so deterministically for the same inputs. Non-deterministic deletion decisions will produce inconsistent results after WAL recovery."
L3. user_key_to_skip String Copy in MergeUntil -- db/merge_helper.cc
  • Issue: The at-bottom deletion path copies the user key into std::string user_key_to_skip before clearing keys_. This is necessary (as documented in the code comment) because orig_ikey.user_key is a Slice backed by keys_.back(). However, this allocates on the heap for every merge-resolved deletion at the bottommost level.
  • Suggested fix: This is acceptable since the at-bottom compaction path is not a hot per-read path. If it becomes a concern, the user key could be compared against the iterator's current key directly without a copy, but the current approach is safer and clearer.
L4. #ifdef NDEBUG Test Guard -- db/db_merge_operator_test.cc
  • Issue: MergeDeletionOpFailureScopeIgnored is guarded by #ifdef NDEBUG, meaning it only runs in release builds. Since unit tests are typically built in debug mode, this test may never run in CI.
  • Suggested fix: Consider restructuring to use ASSERT_DEBUG_DEATH for the debug-mode assertion case, and ASSERT_TRUE(s.IsNotFound()) for the release-mode behavior case, so both are tested.

Cross-Component Analysis

Context Addressed in Diff? Notes
Get/MultiGet point lookups Yes GetContext::PostprocessMerge (inferred from truncated diff + test assertions)
Forward iteration (DBIter) Yes FindNextUserEntryInternal deletion-skip loop
Backward iteration (DBIter) Yes FindValueForCurrentKey + FindValueForCurrentKeyUsingSeek
Compaction (bottommost) Yes MergeUntil drops key + skips older entries
Compaction (non-bottommost) Yes MergeUntil emits kTypeDeletion tombstone
Merge-on-write (max_successive_merges) Yes write_batch.cc MergeCF handles kTypeDeletion
Snapshots Yes Tests verify snapshot isolation
WAL recovery Yes Test MergeDeletionRecovery covers this
WriteBatchWithIndex Uncertain Tests exist; code changes in truncated diff
Transactions Uncertain Test MergeDeletionTransactionGet exists
Blob storage Yes Test MergeDeletionWithBlobStorage + MergeWithBlobBaseValue fix
Wide columns Yes Test MergeDeletionOverWideColumnEntity
Range tombstones Yes Test MergeDeletionWithRangeTombstones
Multi-CF Yes Test MergeDeletionMultiColumnFamilies
Prefix bloom Yes Test MergeDeletionWithPrefixBloom
Compaction filter Yes Test MergeDeletionWithCompactionFilter
User-defined timestamps No Not tested
ReadOnly DB No Not tested
PartialMerge No Not tested

Reconciliation Notes

Many agent findings were initially classified as HIGH/CRITICAL but were reconciled as false positives after careful diff analysis:

  1. "Missing kTypeDeletion in SetValueAndColumnsFromMergeResult" -- Present in diff (db/db_iter.cc)
  2. "Version::MultiGet unchecked PinSelf" -- Fixed in diff (db/version_set.cc wraps in status->ok() check)
  3. "PostprocessMerge corruption bug" -- Likely fixed in truncated diff (get_context.cc); test MergeDeletionSingleLevelGetReturnsNotFound explicitly asserts ASSERT_FALSE(s.IsCorruption()))
  4. "Tombstone sequence number incorrect" -- Correct; uses newest merge operand's sequence, matching compaction semantics
  5. "Bottommost level drops snapshot-protected keys" -- CompactionIterator handles snapshot filtering before calling MergeUntil
  6. "user_key_to_skip copy unnecessary" -- Necessary; documented in code

Positive Observations

  • Thorough iterator coverage: The diff handles all iterator paths (forward, backward, seek, direction change, seek-optimization fast path) with careful attention to pinned data lifecycle (ReleaseTempPinnedData in the deletion-skip loop).
  • Well-commented code: Every significant code change includes detailed comments explaining the "why" (e.g., the user_key_to_skip copy rationale, the at-bottom vs non-bottom distinction, the crash-recovery contract in write_batch.cc).
  • Defensive assertions: The debug-mode assertion that op_failure_scope == kDefault when monostate is set catches confused merge operators early.
  • Comprehensive stress testing: MergeDeletionStress and MergeDeletionConcurrentReadersAndWriters provide good probabilistic coverage.
  • Cross-feature tests: 16 cross-feature tests covering WBWI, transactions, range tombstones, blob storage, wide columns, prefix bloom, compaction filter, and max_successive_merges demonstrate thoroughness.
  • Correct compaction semantics: The at-bottom path correctly skips older entries (preventing resurrection of old values after merge-deletion), and the non-bottom path emits a standard kTypeDeletion tombstone.

ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/code_review.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch from e18bb9b to 95c31c6 Compare June 1, 2026 07:24
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codex Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

🟡 Codex Code Review

Auto-triggered after CI passed — reviewing commit 95c31c6


Codex review failed before producing findings.

WARNING: proceeding, even though we could not update PATH: Refusing to create helper binaries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/codex-home"))
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'

Usage: codex exec review --commit <SHA> --base <BRANCH> --title <TITLE> --model <MODEL> --config <key=value> --dangerously-bypass-approvals-and-sandbox --output-last-message <FILE> [PROMPT]

For more information, try '--help'.

ℹ️ About this response

Generated by Codex CLI.
Review methodology: claude_md/code_review.md

Limitations:

  • Codex may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /codex-review [context] — Request a code review
  • /codex-query <question> — Ask about the PR or codebase
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Claude Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit 95c31c6


Summary

Well-designed feature that adds merge-operator-driven deletion via std::monostate in FullMergeV3. The implementation touches the right subsystems (merge resolution, iterator, compaction, merge-on-write) and the ~1900 lines of tests are thorough. The diff is truncated (31 files, 2624 insertions, but not all shown), so some findings may be addressed in the unseen portion. Findings below are conditioned on what is visible.

High-severity findings (2):

  • [get_context.cc:679] PostprocessMerge must handle Status::NotFound(kMergeOperatorDeletion) -- if not updated, SST-level Get() would return Status::Corruption instead of NotFound for merge-deleted keys. (Likely fixed in truncated diff since tests assert !IsCorruption().)
  • [merge_operator.h:191] NewValue variant must add std::monostate at the end (index 3) to preserve existing variant indices for backwards compatibility. (Not visible in diff; verify placement.)
Full review (click to expand)

Findings

🔴 HIGH

H1. GetContext::PostprocessMerge must handle kMergeOperatorDeletion -- get_context.cc:679
  • Issue: The second TimedFullMergeImpl overload (used by GetContext for point lookups via SST files) returns Status::NotFound(SubCode::kMergeOperatorDeletion) for monostate. The current PostprocessMerge only checks for kMergeOperatorFailed; all other non-OK statuses fall through to state_ = kCorrupt. This means SST-level merge-to-deletion would be reported as Status::Corruption("corrupted key for ...") via Version::Get (line 2872).
  • Root cause: PostprocessMerge was written when merges could only succeed (OK) or fail (Corruption). The new NotFound outcome is a third category.
  • Suggested fix: Add a branch: if (merge_status.IsNotFound()) { state_ = kDeleted; return merge_status; } before the corruption fallthrough. The caller in SaveValue already handles kDeleted state correctly (line 626-640).
  • Confidence: HIGH (3+ agents flagged independently). The PR's test MergeDeletionSingleLevelGetReturnsNotFound explicitly asserts !IsCorruption(), which would fail without this fix. This is almost certainly addressed in the truncated portion of the diff.
H2. NewValue variant ordering for backwards compatibility -- merge_operator.h:191
  • Issue: The current NewValue is std::variant<std::string, NewColumns, Slice>. If std::monostate is inserted at position 0, the variant indices for existing types shift. Code using variant.index() would break silently.
  • Suggested fix: Add std::monostate at the end: std::variant<std::string, NewColumns, Slice, std::monostate>.
  • Confidence: MEDIUM. Most code uses std::visit or type-based access, but this is a public API.

🟡 MEDIUM

M1. Dual-overload behavioral inconsistency in TimedFullMergeImpl -- merge_helper.cc
  • Issue: First overload returns Status::OK() with result_type = kTypeDeletion; second returns Status::NotFound(kMergeOperatorDeletion). Correct but asymmetric -- document clearly.
M2. MergeUntil skip-loop: dangling orig_ikey after keys_.clear() -- merge_helper.cc
  • Issue: orig_ikey.user_key becomes a dangling Slice after keys_.clear(). The PR correctly snapshots into user_key_to_skip beforehand, but orig_ikey itself is now unsafe. A future refactor could accidentally read it.
  • Suggested fix: Defensively null out orig_ikey.user_key after the snapshot.
M3. FindValueForCurrentKeyUsingSeek: removed defensive valid_=true -- db_iter.cc:1580
  • Issue: Correct but fragile. All merge paths must now go through SetValueAndColumnsFromMergeResult. The assert(status_.ok()) only catches violations in debug builds.
M4. Build system completeness -- src.mk, Makefile
  • Issue: counter_delete.cc not found in src.mk. May be in truncated diff.
M5. Debug-only assert for op_failure_scope + monostate -- merge_helper.cc
  • Issue: Release builds silently ignore when both op_failure_scope and monostate are set.

🟢 LOW / NIT

L1. Missing test: PartialMerge + eventual monostate resolution
L2. Missing test: user-defined timestamps + monostate
L3. Missing test: ReadOnly DB / Secondary instance with monostate

Cross-Component Analysis

Context Handling Verified?
WritePreparedTxnDB Merge at read time, unaffected No explicit test
BlobDB Explicit test exists YES
Snapshots Multiple tests YES
WBWI / Transactions Explicit tests YES
max_successive_merges Explicit test YES
Prefix seek Explicit test YES

Positive Observations

  • Thorough test suite (~1900 lines) covering 40+ scenarios
  • Correct use-after-free prevention in MergeUntil skip-loop
  • Proper pinned-data lifecycle management in iterator deletion loop
  • Consistent perf counter bumping in both iteration directions
  • Correct WAL replay contract documentation

ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/code_review.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch from 95c31c6 to ea3d3ce Compare June 1, 2026 08:32
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Codex Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

🟡 Codex Code Review

Auto-triggered after CI passed — reviewing commit ea3d3ce


Codex review failed before producing findings.

WARNING: proceeding, even though we could not update PATH: Refusing to create helper binaries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/codex-home"))
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'

Usage: codex exec review --commit <SHA> --base <BRANCH> --title <TITLE> --model <MODEL> --config <key=value> --dangerously-bypass-approvals-and-sandbox --output-last-message <FILE> [PROMPT]

For more information, try '--help'.

ℹ️ About this response

Generated by Codex CLI.
Review methodology: claude_md/code_review.md

Limitations:

  • Codex may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /codex-review [context] — Request a code review
  • /codex-query <question> — Ask about the PR or codebase
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

Claude Code Review - OBSOLETE

Superseded by a newer AI review. Expand to see the original review.

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit ea3d3ce


Summary

Overall solid PR implementing merge-operator-driven deletion via std::monostate. The design cleanly separates the two return conventions (OK+kTypeDeletion for iterators/compaction, NotFound for point lookups) and the test coverage is extensive. The diff was truncated (31 files, ~15 shown), so some critical changes (status.h, merge_operator.h, get_context.cc updates) are assumed present.

High-severity findings (2):

  • [db/merge_helper.cc:~555] MergeUntil at-bottom skip-loop allocates std::string user_key_to_skip on the compaction hot path; consider reusing a scratch buffer in MergeHelper to avoid heap churn.
  • [db/db_iter.cc:1630] MergeWithBlobBaseValue sets valid_ = true before calling MergeWithPlainBaseValue; verify the diff removes it as described (the diff comment says "do NOT speculatively set valid_ = true").
Full review (click to expand)

Findings

🔴 HIGH

H1. Heap allocation in MergeUntil at-bottom skip-loop -- db/merge_helper.cc:~555
  • Issue: The skip-loop copies orig_ikey.user_key into std::string user_key_to_skip before clearing keys_. Correct for memory safety but introduces a heap allocation on every merge-resolved deletion during compaction.
  • Suggested fix: Consider reusing a scratch buffer in MergeHelper (member variable) instead of a local std::string.
H2. MergeWithBlobBaseValue premature valid_ = true -- db/db_iter.cc:1630
  • Issue: Current code sets valid_ = true before merge. If the diff removes it (as described), this is fine. If not, a stale valid_ = true leaks past deletion.
  • Suggested fix: Confirm the diff removes line 1630's valid_ = true.

🟡 MEDIUM

M1. Missing test coverage for user-defined timestamps
  • Issue: No test exercises merge-operator deletion with UDT enabled. The MergeUntil skip-loop has timestamp-aware logic that is untested for the deletion case.
M2. Missing db_stress coverage
  • Issue: CounterDeleteMergeOperator not integrated into db_stress/db_crashtest.py.
M3. FindValueForCurrentKeyUsingSeek contract change -- db/db_iter.cc:1580
  • Issue: Replaced valid_ = true with assert(status_.ok()). All paths through this function go through SetValueAndColumnsFromMergeResult, so valid_ is correctly set. Callers handle valid_ == false. Appears safe but is a semantic shift.
M4. version_set.cc MultiGet PinnableSlice reset
  • Issue: Defensive iter->value->Reset() on deletion is correct. Monostate visitor returns NotFound without touching output params -- confirmed safe.

🟢 LOW / NIT

L1. Debug-only assert for op_failure_scope -- db/merge_helper.cc:~115
  • Consider ROCKS_LOG_WARN in release builds for confused merge operators.
L2. Slice("", 0) for synthesized tombstone -- safe, well-documented.
L3. Consistent PERF_COUNTER_ADD for deletion in both directions -- good.
L4. RandomSeedWithTrace naming -- doesn't actually do the trace itself.

Cross-Component Analysis

Context Affected? Analysis
WritePreparedTxnDB Yes Safe -- visibility is upstream of merge resolution
ReadOnly DB Yes Works if PostprocessMerge updated (truncated diff)
User-defined timestamps Yes Untested (M1)
BlobDB Yes Tested. Works.
FIFO/Universal compaction Yes Same CompactionIterator path. Safe.
MemPurge Possibly Untested but should propagate correctly
Snapshots Yes Tested extensively. Skip-loop respects boundaries.

Positive Observations

  • Thorough test suite (40+ cases) covering iteration, snapshots, WBWI, transactions, blobs, wide columns, prefix bloom, concurrent access
  • Clean two-overload design separating iterator/compaction vs point-lookup return conventions
  • Careful memory management with explicit user_key copy before keys_.clear()
  • Well-documented code changes throughout

ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/code_review.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch from ea3d3ce to 68bfca7 Compare June 1, 2026 12:39
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

🟡 Codex Code Review

Auto-triggered after CI passed — reviewing commit 68bfca7


Codex review failed before producing findings.

WARNING: proceeding, even though we could not update PATH: Refusing to create helper binaries under temporary dir "/tmp" (codex_home: AbsolutePathBuf("/tmp/codex-home"))
error: the argument '--base <BRANCH>' cannot be used with '[PROMPT]'

Usage: codex exec review --commit <SHA> --base <BRANCH> --title <TITLE> --model <MODEL> --config <key=value> --dangerously-bypass-approvals-and-sandbox --output-last-message <FILE> [PROMPT]

For more information, try '--help'.

ℹ️ About this response

Generated by Codex CLI.
Review methodology: claude_md/code_review.md

Limitations:

  • Codex may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /codex-review [context] — Request a code review
  • /codex-query <question> — Ask about the PR or codebase
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

✅ Claude Code Review

Auto-triggered after CI passed — reviewing commit 68bfca7


Summary

This PR adds std::monostate as a fourth alternative to MergeOperationOutputV3::NewValue, enabling merge operators to signal key deletion. The visible diff covers core logic in MergeHelper, DBIter, CompactionIterator, and extensive tests (~1900 lines). The diff is truncated (32 files, only a subset visible), so several critical infrastructure changes are not shown. The review below evaluates the visible changes against the current codebase, flagging both issues in the visible diff and verifiable gaps in unseen files.

High-severity findings (3):

  • [include/rocksdb/status.h] kMergeOperatorDeletion subcode and MergeOperationOutputV3::NewValue variant must be added (not visible in truncated diff); without them GetContext::PostprocessMerge will misclassify merge-deletion as corruption.
  • [table/get_context.cc:679] PostprocessMerge must be updated to handle Status::NotFound(kMergeOperatorDeletion) -- current code falls through to state_ = kCorrupt.
  • [db/version_set.cc:3155] Version::MultiGet calls iter->value->PinSelf() after TimedFullMerge without checking for merge-deletion NotFound status.
Full review (click to expand)

Findings

🔴 HIGH

H1. Missing infrastructure changes (truncated diff) -- include/rocksdb/merge_operator.h, include/rocksdb/status.h, util/status.cc
  • Issue: The current MergeOperationOutputV3::NewValue is std::variant<std::string, NewColumns, Slice> (3 types). The Status::SubCode enum has no kMergeOperatorDeletion entry, and util/status.cc has no corresponding message string. These are prerequisites for the entire feature.
  • Root cause: The diff is truncated at 32 files. These changes almost certainly exist in the full PR but are not visible for review.
  • Suggested fix: Verify the full PR includes: (1) std::monostate added to NewValue variant (ABI-breaking -- document in HISTORY.md), (2) kMergeOperatorDeletion added before kMaxSubCode, (3) message string added to msgs[], (4) Java/JNI bindings updated.
H2. GetContext::PostprocessMerge must handle merge-deletion -- table/get_context.cc:679
  • Issue: When TimedFullMerge returns Status::NotFound(kMergeOperatorDeletion), PostprocessMerge enters !merge_status.ok(). Only kMergeOperatorFailed is special-cased; everything else sets state_ = kCorrupt. Point lookups via SST would return Corruption instead of NotFound.
  • Suggested fix: Check for merge_status.IsNotFound() with subcode kMergeOperatorDeletion and set state_ = kDeleted.
H3. Version::MultiGet unconditional value processing -- db/version_set.cc:3155
  • Issue: After TimedFullMerge, iter->value->PinSelf() and range->AddValueSize() execute without checking for merge-deletion NotFound.
  • Suggested fix: Check status before processing value; mark key done with NotFound for deletions.

🟡 MEDIUM

M1. FindValueForCurrentKeyUsingSeek removes valid_ = true -- db/db_iter.cc:1580

Relies on SetValueAndColumnsFromMergeResult having set valid_. Correct but fragile; consider documenting the invariant.

M2. Forward iteration deletion-skip relies on iter_ positioning -- db/db_iter.cc:688-710

The continue skips iter_.Next(), relying on MergeValuesNewToOld having advanced past all entries. Correct but add assertion.

M3. Memtable merge-on-write path -- db/write_batch.cc

The !merge_status.ok() check would treat deletion NotFound as failure. Must be updated (likely in truncated diff).

M4. MergeUntil user_key_to_skip_ -- correct use-after-free prevention. Sound approach.

🟢 LOW / NIT

  • L1: No specific compaction stat for merge-resolved deletions
  • L2: OpFailureScope assert (debug) vs test (release) -- acceptable
  • L3: RandomSeedWithTrace name misleading (doesn't call SCOPED_TRACE itself)
  • L4: Slice("", 0) for tombstone body -- fine, static lifetime

Cross-Component Analysis

Context Assessment
WritePreparedTxnDB Safe -- deletion resolves before visibility checks
User-defined timestamps Safe -- skip loop uses matching comparison logic; tested
BlobDB Safe -- delegates correctly; tested
ReadOnly DB Safe -- read-path only
Concurrent writes Safe -- per-thread resolution; tested

Positive Observations

  • Extensive test coverage (~1900 lines): iteration, snapshots, compaction, UDT, WAL recovery, WBWI, transactions, blobs, wide columns, range tombstones, multi-CF, prefix bloom, concurrent access
  • Correct use-after-free prevention via user_key_to_skip_
  • SetValueAndColumnsFromMergeResult as single authority for valid_ from merge results
  • Both forward and backward seek-optimization paths covered

The full review has been written to review-findings.md.


ℹ️ About this response

Generated by Claude Code.
Review methodology: claude_md/code_review.md

Limitations:

  • Claude may miss context from files not in the diff
  • Large PRs may be truncated
  • Always apply human judgment to AI suggestions

Commands:

  • /claude-review [context] — Request a code review
  • /claude-query <question> — Ask about the PR or codebase
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch from 68bfca7 to 4108990 Compare June 1, 2026 15:06
Add std::monostate as a fourth alternative to
MergeOperationOutputV3::NewValue. When a merge operator sets this
variant, the key is treated as deleted:

- Get()/MultiGet() return Status::NotFound()
- Iterators skip the key (forward and backward)
- Compaction produces a deletion tombstone, or drops the key entirely
  when the full history has been seen at the bottommost level

This enables use cases like counter auto-delete-at-zero, conditional
mutations, and Cassandra-style row expiration without requiring a
separate compaction filter.

Closes facebook#14593
@zaidoon1 zaidoon1 force-pushed the merge-operator-deletion-result branch from 4108990 to 919cdda Compare June 1, 2026 15:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants