Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a3bc6c9
feat: add range tombstones (delete_range / delete_prefix)
polaz Mar 16, 2026
c3e80fc
Merge branch 'main' into feat/#16-feat-range-tombstones--deleterange-…
polaz Mar 16, 2026
ed272cf
fix: resolve all clippy warnings for range tombstone code
polaz Mar 16, 2026
718f2ba
fix(range-tombstone): validate bounds, fix RT-only flush and edge cases
polaz Mar 16, 2026
343f31c
fix(table): validate BlockType on range tombstone block load
polaz Mar 16, 2026
3e23f57
fix(range-tombstone): seqno visibility, decode hardening, lint attrs
polaz Mar 16, 2026
954c044
fix(lint): use cfg_attr(feature, expect) for metrics-gated arg count
polaz Mar 16, 2026
d53ecfe
fix(range-tombstone): propagate RTs before write loop, enforce u16 bo…
polaz Mar 16, 2026
cb60d63
test(range-tombstone): rotation, blob tree, table-skip, invalid interval
polaz Mar 16, 2026
e1db06d
fix(range-tombstone): RT-only SST persistence, pruning, lint attrs
polaz Mar 16, 2026
f0c90ea
style: fix rustfmt formatting in interval_tree
polaz Mar 17, 2026
1f87efa
Merge branch 'main' into feat/#16-feat-range-tombstones--deleterange-…
polaz Mar 17, 2026
334890c
fix(range-tombstone): preserve sentinel seqno bounds, soft-reject ove…
polaz Mar 17, 2026
5077e58
chore: remove .forge from git tracking
polaz Mar 17, 2026
6841260
chore: add .claude to .gitignore
polaz Mar 17, 2026
9522778
chore: merge main into feature branch
polaz Mar 17, 2026
df7c0f4
fix(range-tombstone): use #[expect] lints, optimize query_suppression
polaz Mar 17, 2026
9a67ff2
fix(interval-tree): remove unfired unnecessary_box_returns expects
polaz Mar 17, 2026
51c3429
fix(range-tombstone): preserve sentinel seqno bounds, soft-reject ove…
polaz Mar 17, 2026
86a985f
docs(test): clarify Guard import is a trait dependency for .key()
polaz Mar 17, 2026
e525c10
docs(test): clarify Vec<Vec<u8>> PartialEq coercion for assertions
polaz Mar 17, 2026
12a23c6
fix(range-tombstone): correct flush clipping and RT-only metadata range
polaz Mar 17, 2026
2dbe5c6
fix(range-tombstone): write all RTs in flush mode without overlap filter
polaz Mar 17, 2026
28d419f
test(range-tombstone): add disjoint RT flush and compaction tests
polaz Mar 17, 2026
e59eb29
fix(range-tombstone): use max RT seqno for sentinel to avoid seqno=0 …
polaz Mar 17, 2026
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
71 changes: 68 additions & 3 deletions src/abstract_tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ pub trait AbstractTree {
_lock: &MutexGuard<'_, ()>,
seqno_threshold: SeqNo,
) -> crate::Result<Option<u64>> {
use crate::{compaction::stream::CompactionStream, merge::Merger};
use crate::{
compaction::stream::CompactionStream, merge::Merger, range_tombstone::RangeTombstone,
};

let version_history = self.get_version_history_lock();
let latest = version_history.latest_version();
Expand All @@ -93,6 +95,13 @@ pub trait AbstractTree {

let flushed_size = latest.sealed_memtables.iter().map(|mt| mt.size()).sum();

// Collect range tombstones from sealed memtables
let mut range_tombstones: Vec<RangeTombstone> = Vec::new();
for mt in latest.sealed_memtables.iter() {
range_tombstones.extend(mt.range_tombstones_sorted());
}
range_tombstones.sort();

let merger = Merger::new(
latest
.sealed_memtables
Expand All @@ -104,7 +113,21 @@ pub trait AbstractTree {

drop(version_history);

if let Some((tables, blob_files)) = self.flush_to_tables(stream)? {
// Clone needed: flush_to_tables_with_rt consumes the Vec, but on the
// RT-only path (no KV data, tables.is_empty()) we re-insert RTs into the
// active memtable. Flush is infrequent and RT count is small.
if let Some((tables, blob_files)) =
self.flush_to_tables_with_rt(stream, range_tombstones.clone())?
{
// If no tables were produced (RT-only memtable), re-insert RTs
// into active memtable so they aren't lost
if tables.is_empty() && !range_tombstones.is_empty() {
let active = self.active_memtable();
for rt in &range_tombstones {
active.insert_range_tombstone(rt.start.clone(), rt.end.clone(), rt.seqno);
}
}

self.register_tables(
&tables,
blob_files.as_deref(),
Expand Down Expand Up @@ -216,10 +239,22 @@ pub trait AbstractTree {
/// # Errors
///
/// Will return `Err` if an IO error occurs.
#[warn(clippy::type_complexity)]
fn flush_to_tables(
&self,
stream: impl Iterator<Item = crate::Result<InternalValue>>,
) -> crate::Result<Option<FlushToTablesResult>> {
self.flush_to_tables_with_rt(stream, Vec::new())
}

/// Like [`AbstractTree::flush_to_tables`], but also writes range tombstones.
///
/// # Errors
///
/// Will return `Err` if an IO error occurs.
fn flush_to_tables_with_rt(
&self,
stream: impl Iterator<Item = crate::Result<InternalValue>>,
range_tombstones: Vec<crate::range_tombstone::RangeTombstone>,
) -> crate::Result<Option<FlushToTablesResult>>;

/// Atomically registers flushed tables into the tree, removing their associated sealed memtables.
Expand Down Expand Up @@ -680,4 +715,34 @@ pub trait AbstractTree {
/// Will return `Err` if an IO error occurs.
#[doc(hidden)]
fn remove_weak<K: Into<UserKey>>(&self, key: K, seqno: SeqNo) -> (u64, u64);

/// Deletes all keys in the range `[start, end)` by inserting a range tombstone.
///
/// This is much more efficient than deleting keys individually when
/// removing a contiguous range of keys.
///
/// Returns the approximate size added to the memtable.
/// Returns 0 if `start >= end` (invalid interval is silently ignored).
fn remove_range<K: Into<UserKey>>(&self, start: K, end: K, seqno: SeqNo) -> u64;

/// Deletes all keys with the given prefix by inserting a range tombstone.
///
/// This is sugar over [`AbstractTree::remove_range`] using prefix bounds.
///
/// Returns the approximate size added to the memtable.
/// Returns 0 for empty prefixes or all-`0xFF` prefixes (cannot form valid half-open range).
fn remove_prefix<K: AsRef<[u8]>>(&self, prefix: K, seqno: SeqNo) -> u64 {
use crate::range::prefix_to_range;
use std::ops::Bound;

let (lo, hi) = prefix_to_range(prefix.as_ref());

let Bound::Included(start) = lo else { return 0 };

// Bound::Unbounded means the prefix is all 0xFF — no representable
// exclusive upper bound exists, so we cannot form a valid range tombstone.
let Bound::Excluded(end) = hi else { return 0 };

self.remove_range(start, end, seqno)
}
}
Loading
Loading