mod blob_file_list;
mod diff;
pub mod edit;
pub mod edit_log;
mod framing;
mod optimize;
mod persist;
pub mod recovery;
pub mod run;
mod super_version;
#[cfg(test)]
mod tests;
pub use blob_file_list::BlobFileList;
pub use persist::persist_version;
pub use run::Run;
pub use super_version::RetentionEffect;
pub use super_version::SnapshotRef;
pub use super_version::{SuperVersion, SuperVersions};
use crate::TreeType;
use crate::blob_tree::{FragmentationEntry, FragmentationMap};
use crate::checksum::ChecksumType;
use crate::coding::Encode;
use crate::compaction::state::hidden_set::HiddenSet;
use crate::version::recovery::Recovery;
use crate::{
HashSet, KeyRange, Table, TableId,
comparator::UserComparator,
vlog::{BlobFile, BlobFileId},
};
use alloc::sync::Arc;
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use core::ops::Deref;
use optimize::optimize_runs;
use run::Ranged;
fn place_intra_l0_output(runs: Vec<Run<Table>>, output: Run<Table>) -> Vec<Run<Table>> {
let output_recency = output.iter().map(Table::l0_recency).max();
let landed_since = |table: &Table| {
matches!(
(table.metadata.recency, output_recency),
(Some(recency), Some(output)) if recency > output
)
};
let mut ahead = Vec::new();
let mut behind = Vec::with_capacity(runs.len());
for mut run in runs {
let (newer, older): (Vec<Table>, Vec<Table>) = core::mem::take(run.inner_mut())
.into_iter()
.partition(|table| landed_since(table));
ahead.extend(newer.into_iter().filter_map(|table| Run::new(vec![table])));
behind.extend(Run::new(older));
}
ahead.push(output);
ahead.extend(behind);
ahead
}
pub struct TransformContext<'a> {
comparator: &'a dyn UserComparator,
}
impl<'a> TransformContext<'a> {
pub fn new(comparator: &'a dyn UserComparator) -> Self {
Self { comparator }
}
pub fn comparator(&self) -> &'a dyn UserComparator {
self.comparator
}
}
pub const DEFAULT_LEVEL_COUNT: u8 = 7;
pub type VersionId = u64;
impl Ranged for Table {
fn key_range(&self) -> &KeyRange {
&self.metadata.key_range
}
}
pub struct GenericLevel<T: Ranged> {
runs: Vec<Arc<Run<T>>>,
}
impl<T: Ranged> core::ops::Deref for GenericLevel<T> {
type Target = [Arc<Run<T>>];
fn deref(&self) -> &Self::Target {
&self.runs
}
}
impl<T: Ranged> GenericLevel<T> {
pub fn new(runs: Vec<Arc<Run<T>>>) -> Self {
Self { runs }
}
pub fn table_count(&self) -> usize {
self.iter().map(|x| x.len()).sum()
}
pub fn run_count(&self) -> usize {
self.runs.len()
}
pub fn is_disjoint(&self) -> bool {
self.run_count() == 1
}
pub fn is_empty(&self) -> bool {
self.runs.is_empty()
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Arc<Run<T>>> {
self.runs.iter()
}
}
#[derive(Clone)]
pub struct Level(Arc<GenericLevel<Table>>);
impl core::ops::Deref for Level {
type Target = GenericLevel<Table>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Level {
pub fn empty() -> Self {
Self::from_runs(vec![])
}
pub fn from_runs(runs: Vec<Arc<Run<Table>>>) -> Self {
Self(Arc::new(GenericLevel { runs }))
}
pub fn list_ids(&self) -> HashSet<TableId> {
self.iter()
.flat_map(|run| run.iter())
.map(Table::id)
.collect()
}
pub fn first_run(&self) -> Option<&Arc<Run<Table>>> {
self.runs.first()
}
pub fn size(&self) -> u64 {
self.0
.iter()
.flat_map(|x| x.iter())
.map(Table::file_size)
.sum()
}
pub fn aggregate_key_range(&self) -> KeyRange {
self.aggregate_key_range_cmp(&crate::comparator::DefaultUserComparator)
}
pub fn aggregate_key_range_cmp<C: crate::comparator::UserComparator + ?Sized>(
&self,
cmp: &C,
) -> KeyRange {
if self.run_count() == 1 {
#[expect(
clippy::expect_used,
reason = "we check for run_count, so the first run must exist"
)]
self.runs
.first()
.expect("should exist")
.aggregate_key_range()
} else {
let key_ranges = self
.iter()
.map(|x| Run::aggregate_key_range(x))
.collect::<Vec<_>>();
KeyRange::aggregate_cmp(key_ranges.iter(), cmp)
}
}
}
pub struct VersionInner {
id: VersionId,
tree_type: TreeType,
levels: Vec<Level>,
#[doc(hidden)]
pub blob_files: Arc<BlobFileList>,
gc_stats: Arc<FragmentationMap>,
retention_floor: crate::SeqNo,
dicts: Arc<[crate::file::DictId]>,
}
#[derive(Clone)]
pub struct Version {
inner: Arc<VersionInner>,
}
impl core::ops::Deref for Version {
type Target = VersionInner;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl Version {
pub fn tree_type(&self) -> TreeType {
self.tree_type
}
pub fn id(&self) -> VersionId {
self.id
}
pub fn gc_stats(&self) -> &FragmentationMap {
&self.gc_stats
}
#[must_use]
pub fn retention_floor(&self) -> crate::SeqNo {
self.retention_floor
}
#[must_use]
pub fn with_retention_floor(&self, floor: crate::SeqNo) -> Self {
if floor <= self.retention_floor {
return self.clone();
}
Self {
inner: Arc::new(VersionInner {
id: self.id,
tree_type: self.tree_type,
levels: self.levels.clone(),
blob_files: self.blob_files.clone(),
gc_stats: self.gc_stats.clone(),
retention_floor: floor,
dicts: self.dicts.clone(),
}),
}
}
#[must_use]
pub fn dicts(&self) -> &[crate::file::DictId] {
&self.dicts
}
#[must_use]
pub fn referenced_dicts(&self) -> Vec<crate::file::DictId> {
fn dict_of(compression: crate::CompressionType) -> Option<crate::file::DictId> {
match compression {
#[cfg(zstd_any)]
crate::CompressionType::ZstdDict { dict_id, .. } => Some(dict_id),
_ => None,
}
}
let mut ids: Vec<crate::file::DictId> = self
.iter_tables()
.filter_map(|table| dict_of(table.metadata.data_block_compression))
.chain(
self.blob_files
.iter()
.filter_map(|bf| dict_of(bf.compression())),
)
.collect();
ids.sort_unstable();
ids.dedup();
ids
}
#[must_use]
pub fn with_dict(&self, id: crate::file::DictId) -> Self {
if self.dicts.contains(&id) {
return self.clone();
}
let mut dicts = self.dicts.to_vec();
let at = dicts.partition_point(|held| *held < id);
dicts.insert(at, id);
self.with_id_and_dicts(self.id + 1, dicts)
}
#[must_use]
pub fn without_dicts(&self, ids: &[crate::file::DictId]) -> Self {
if !self.dicts.iter().any(|held| ids.contains(held)) {
return self.clone();
}
let dicts = self
.dicts
.iter()
.copied()
.filter(|held| !ids.contains(held))
.collect::<Vec<_>>();
self.with_id_and_dicts(self.id + 1, dicts)
}
#[must_use]
pub(crate) fn cleared(&self, still_written: &[crate::file::DictId]) -> Self {
let dicts = self
.dicts
.iter()
.copied()
.filter(|id| still_written.contains(id))
.collect();
Self::new(self.id + 1, self.tree_type).with_dicts(dicts)
}
#[must_use]
pub(crate) fn with_dicts(&self, dicts: Vec<crate::file::DictId>) -> Self {
self.with_id_and_dicts(self.id, dicts)
}
fn with_id_and_dicts(&self, id: VersionId, dicts: Vec<crate::file::DictId>) -> Self {
Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels: self.levels.clone(),
blob_files: self.blob_files.clone(),
gc_stats: self.gc_stats.clone(),
retention_floor: self.retention_floor,
dicts: Arc::from(dicts),
}),
}
}
pub(crate) fn set_retention_floor(&mut self, floor: crate::SeqNo) {
if floor <= self.retention_floor {
return;
}
if let Some(inner) = Arc::get_mut(&mut self.inner) {
inner.retention_floor = floor;
} else {
*self = self.with_retention_floor(floor);
}
}
pub fn l0(&self) -> &Level {
#[expect(clippy::expect_used)]
self.levels.first().expect("L0 should exist")
}
#[must_use]
pub fn level_is_busy(&self, idx: usize, hidden_set: &HiddenSet) -> bool {
self.level(idx).is_some_and(|level| {
level
.iter()
.flat_map(|run| run.iter())
.any(|table| hidden_set.is_hidden(table.id()))
})
}
pub fn new(id: VersionId, tree_type: TreeType) -> Self {
let levels = (0..DEFAULT_LEVEL_COUNT).map(|_| Level::empty()).collect();
Self {
inner: Arc::new(VersionInner {
id,
tree_type,
levels,
blob_files: Arc::default(),
gc_stats: Arc::default(),
retention_floor: 0,
dicts: Arc::from([]),
}),
}
}
pub(crate) fn from_recovery(
recovery: Recovery,
tables: &[Table],
blob_files: &[BlobFile],
) -> crate::Result<Self> {
let version_levels = recovery
.table_ids
.iter()
.map(|level| {
let level_runs = level
.iter()
.map(|run| {
let run_tables = run
.iter()
.map(|table| {
let opened = tables
.iter()
.find(|x| x.id() == table.id)
.cloned()
.ok_or(crate::Error::Unrecoverable)?;
Ok(match recovery.restrictions.get(&table.id) {
Some(bound) => opened.with_restriction(bound.clone()),
None => opened,
})
})
.collect::<crate::Result<Vec<_>>>()?;
Ok(Arc::new(
#[expect(
clippy::expect_used,
reason = "empty runs should not exist, so there should not be any empty persisted runs"
)]
Run::new(run_tables).expect("persisted runs should not be empty"),
))
})
.collect::<crate::Result<Vec<_>>>()?;
Ok(Level::from_runs(level_runs))
})
.collect::<crate::Result<Vec<_>>>()?;
let recovered = Self::from_levels(
recovery.curr_version_id,
recovery.tree_type,
version_levels,
BlobFileList::new(blob_files.iter().cloned().map(|bf| (bf.id(), bf)).collect()),
recovery.gc_stats,
)
.with_retention_floor(recovery.retention_floor);
let mut dicts = recovery.dicts;
dicts.extend(recovered.referenced_dicts());
dicts.sort_unstable();
dicts.dedup();
Ok(recovered.with_dicts(dicts))
}
pub fn from_levels(
id: VersionId,
tree_type: TreeType,
levels: Vec<Level>,
blob_files: BlobFileList,
gc_stats: FragmentationMap,
) -> Self {
Self {
inner: Arc::new(VersionInner {
id,
tree_type,
levels,
blob_files: Arc::new(blob_files),
gc_stats: Arc::new(gc_stats),
retention_floor: 0,
dicts: Arc::from([]),
}),
}
}
pub fn level_count(&self) -> usize {
self.levels.len()
}
pub fn iter_levels(&self) -> impl Iterator<Item = &Level> {
self.levels.iter()
}
pub fn table_count(&self) -> usize {
self.iter_levels().map(|x| x.table_count()).sum()
}
pub fn blob_file_count(&self) -> usize {
self.blob_files.len()
}
pub fn iter_tables(&self) -> impl Iterator<Item = &Table> {
self.levels
.iter()
.flat_map(|x| x.iter())
.flat_map(|x| x.iter())
}
pub(crate) fn get_table(&self, id: TableId) -> Option<&Table> {
self.iter_tables().find(|x| x.metadata.id == id)
}
pub fn level(&self, n: usize) -> Option<&Level> {
self.levels.get(n)
}
pub fn with_new_l0_run(
&self,
run: &[Table],
blob_files: Option<&[BlobFile]>,
diff: Option<FragmentationMap>,
ctx: &TransformContext<'_>,
) -> Self {
let comparator = ctx.comparator;
let id = self.id + 1;
let mut levels = vec![];
levels.push({
#[expect(clippy::expect_used, reason = "L0 always exists")]
let l0 = self.levels.first().expect("L0 should always exist");
let prev_runs = l0
.runs
.iter()
.map(|run| {
let run: Run<_> = run.deref().clone();
run
})
.collect::<Vec<_>>();
let mut runs = Vec::with_capacity(prev_runs.len() + run.len());
runs.extend(run.iter().cloned().map(|table| {
let Some(run) = Run::new(vec![table]) else {
unreachable!("single-table run should never be empty");
};
run
}));
runs.extend(prev_runs);
let runs = optimize_runs(runs, comparator);
Level::from_runs(runs.into_iter().map(Arc::new).collect())
});
levels.extend(self.levels.iter().skip(1).cloned());
let value_log = if let Some(blob_files) = blob_files {
let mut copy = self.blob_files.deref().clone();
copy.extend(blob_files.iter().cloned().map(|bf| (bf.id(), bf)));
copy.into()
} else {
self.blob_files.clone()
};
let gc_stats = if let Some(diff) = diff {
let mut copy = self.gc_stats.deref().clone();
diff.merge_into(&mut copy);
copy.prune(&value_log);
Arc::new(copy)
} else {
self.gc_stats.clone()
};
Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: value_log,
gc_stats,
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
}
}
pub fn with_dropped(
&self,
ids: &[TableId],
dropped_blob_files: &mut Vec<BlobFile>,
ctx: &TransformContext<'_>,
) -> crate::Result<Self> {
let comparator = ctx.comparator;
let id = self.id + 1;
let mut levels = vec![];
let mut dropped_tables: Vec<Table> = vec![];
for level in &self.levels {
let runs = level
.runs
.iter()
.map(|run| {
let mut run: Run<_> = run.deref().clone();
let removed_tables = run
.inner_mut()
.extract_if(.., |x| ids.contains(&x.metadata.id));
dropped_tables.extend(removed_tables);
run
})
.filter(|x| !x.is_empty())
.collect::<Vec<_>>();
let runs = optimize_runs(runs, comparator);
levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect()));
}
let gc_stats = if dropped_tables.is_empty() {
self.gc_stats.clone()
} else {
let mut copy = self.gc_stats.deref().clone();
for table in &dropped_tables {
let linked_blob_files = table.list_blob_file_references()?.unwrap_or_default();
for blob_file in linked_blob_files {
copy.entry(blob_file.blob_file_id)
.and_modify(|counter| {
counter.bytes += blob_file.bytes;
counter.len += blob_file.len;
})
.or_insert_with(|| {
FragmentationEntry::new(
blob_file.len,
blob_file.bytes,
blob_file.on_disk_bytes,
)
});
}
}
Arc::new(copy)
};
let value_log = if dropped_tables.is_empty() {
self.blob_files.clone()
} else {
let mut copy = self.blob_files.deref().clone();
dropped_blob_files.extend(copy.prune_dead(&gc_stats));
Arc::new(copy)
};
Ok(Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: value_log,
gc_stats,
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
})
}
#[expect(
clippy::too_many_arguments,
reason = "merge requires blob/GC params alongside context; further bundling planned"
)]
pub fn with_merge(
&self,
old_ids: &[TableId],
new_tables: &[Table],
dest_level: usize,
diff: Option<FragmentationMap>,
new_blob_files: Vec<BlobFile>,
blob_files_to_drop: &HashSet<BlobFileId>,
ctx: &TransformContext<'_>,
) -> Self {
let comparator = ctx.comparator;
let id = self.id + 1;
let mut levels = vec![];
for (level_idx, level) in self.levels.iter().enumerate() {
let mut runs = level
.runs
.iter()
.map(|run| {
let mut run: Run<_> = run.deref().clone();
run.retain(|x| !old_ids.contains(&x.metadata.id));
run
})
.filter(|x| !x.is_empty())
.collect::<Vec<_>>();
if level_idx == dest_level
&& let Some(run) = Run::new(new_tables.to_vec())
{
let rewritten = match old_ids {
[only] if dest_level == 0 => level
.runs
.iter()
.position(|run| run.iter().any(|table| table.metadata.id == *only)),
_ => None,
};
if let Some(slot) = rewritten {
let shared = level.runs.get(slot).is_some_and(|run| run.len() > 1);
match runs.get_mut(slot) {
Some(at) if shared => {
for table in new_tables {
at.push_cmp(table.clone(), comparator);
}
}
_ => runs.insert(slot, run),
}
} else if dest_level == 0 {
runs = place_intra_l0_output(runs, run);
} else {
runs.insert(0, run);
}
}
let runs = optimize_runs(runs, comparator);
levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect()));
}
let has_diff = diff.is_some();
let value_log = if has_diff || !new_blob_files.is_empty() || !blob_files_to_drop.is_empty()
{
let mut copy = self.blob_files.deref().clone();
for blob_file in new_blob_files {
copy.insert(blob_file.id(), blob_file);
}
for &id in blob_files_to_drop {
copy.remove(id);
}
Arc::new(copy)
} else {
self.blob_files.clone()
};
let gc_stats = if has_diff || !blob_files_to_drop.is_empty() {
let mut copy = self.gc_stats.deref().clone();
if let Some(diff) = diff {
diff.merge_into(&mut copy);
}
copy.prune(&value_log);
Arc::new(copy)
} else {
self.gc_stats.clone()
};
Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: value_log,
gc_stats,
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
}
}
pub fn with_moved(
&self,
ids: &[TableId],
dest_level: usize,
ctx: &TransformContext<'_>,
) -> Self {
let comparator = ctx.comparator;
let id = self.id + 1;
let mut affected_tables = self
.iter_tables()
.filter(|x| ids.contains(&x.id()))
.cloned()
.collect::<Vec<_>>();
assert_eq!(affected_tables.len(), ids.len(), "invalid table IDs");
affected_tables
.sort_by(|a, b| comparator.compare(a.key_range().min(), b.key_range().min()));
let mut levels = vec![];
for (level_idx, level) in self.levels.iter().enumerate() {
let mut runs = level
.runs
.iter()
.map(|run| {
let mut run: Run<_> = run.deref().clone();
run.retain(|x| !ids.contains(&x.metadata.id));
run
})
.filter(|x| !x.is_empty())
.collect::<Vec<_>>();
if level_idx == dest_level
&& let Some(run) = Run::new(affected_tables.clone())
{
runs.insert(0, run);
}
let runs = optimize_runs(runs, comparator);
levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect()));
}
Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: self.blob_files.clone(),
gc_stats: self.gc_stats.clone(),
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
}
}
#[must_use]
pub(crate) fn with_refreshed_table_checksum(
&self,
table_id: TableId,
checksum: crate::checksum::Checksum,
) -> Option<Self> {
if !self.iter_tables().any(|t| t.id() == table_id) {
return None;
}
let id = self.id + 1;
let mut levels = vec![];
for level in &self.levels {
let runs = level
.runs
.iter()
.map(|run| {
if run.iter().any(|t| t.id() == table_id) {
let mut run: Run<_> = run.deref().clone();
for t in run.inner_mut().iter_mut() {
if t.id() == table_id {
*t = t.with_refreshed_checksum(checksum);
}
}
Arc::new(run)
} else {
run.clone()
}
})
.collect::<Vec<_>>();
levels.push(Level::from_runs(runs));
}
Some(Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: self.blob_files.clone(),
gc_stats: self.gc_stats.clone(),
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
})
}
#[expect(
clippy::too_many_arguments,
reason = "a slice install carries the SST swaps/removes/outputs plus the \
KV-separation blob delta (new files + GC diff); bundling would \
just move the argument list"
)]
pub fn with_tight_slice(
&self,
restricted: &[(TableId, Table)],
removed_ids: &[TableId],
outputs: &[Table],
new_blob_files: Vec<BlobFile>,
gc_diff: Option<FragmentationMap>,
dest_level: usize,
ctx: &TransformContext<'_>,
) -> Self {
let comparator = ctx.comparator;
let id = self.id + 1;
let mut levels = vec![];
for (level_idx, level) in self.levels.iter().enumerate() {
let mut runs = level
.runs
.iter()
.map(|run| {
let mut run: Run<_> = run.deref().clone();
run.retain(|t| !removed_ids.contains(&t.id()));
for t in run.inner_mut().iter_mut() {
if let Some((_, view)) = restricted.iter().find(|(rid, _)| *rid == t.id()) {
*t = view.clone();
}
}
run
})
.filter(|run| !run.is_empty())
.collect::<Vec<_>>();
if level_idx == dest_level
&& let Some(run) = Run::new(outputs.to_vec())
{
if dest_level == 0 {
runs.push(run);
} else {
runs.insert(0, run);
}
}
let runs = optimize_runs(runs, comparator);
levels.push(Level::from_runs(runs.into_iter().map(Arc::new).collect()));
}
let value_log = if gc_diff.is_some() || !new_blob_files.is_empty() {
let mut copy = self.blob_files.deref().clone();
for blob_file in new_blob_files {
copy.insert(blob_file.id(), blob_file);
}
Arc::new(copy)
} else {
self.blob_files.clone()
};
let gc_stats = if let Some(diff) = gc_diff {
let mut copy = self.gc_stats.deref().clone();
diff.merge_into(&mut copy);
Arc::new(copy)
} else {
self.gc_stats.clone()
};
Self {
inner: Arc::new(VersionInner {
id,
tree_type: self.tree_type,
levels,
blob_files: value_log,
gc_stats,
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
}
}
}
impl Version {
pub(crate) fn fits_snapshot(&self) -> bool {
self.iter_levels()
.all(|level| u8::try_from(level.run_count()).is_ok())
}
pub(crate) fn snapshot_base(&self) -> Self {
let levels = self
.levels
.iter()
.map(|level| {
if u8::try_from(level.run_count()).is_ok() {
level.clone()
} else {
Level::empty()
}
})
.collect();
Self {
inner: Arc::new(VersionInner {
id: self.id,
tree_type: self.tree_type,
levels,
blob_files: self.blob_files.clone(),
gc_stats: self.gc_stats.clone(),
retention_floor: self.retention_floor,
dicts: self.dicts.clone(),
}),
}
}
pub(crate) fn encode_into(
&self,
writer: &mut crate::manifest_blocks::writer::ManifestArchiveWriter,
comparator_name: &str,
) -> Result<(), crate::Error> {
use crate::FormatVersion;
#[cfg(not(feature = "std"))]
use crate::io::Write;
use crate::io::{LittleEndian, WriteBytesExt};
#[cfg(feature = "std")]
use std::io::Write;
writer.start("format_version")?;
writer.write_u8(FormatVersion::V5.into())?;
writer.start("crate_version")?;
writer.write_all(env!("CARGO_PKG_VERSION").as_bytes())?;
writer.start("tree_type")?;
writer.write_u8(self.tree_type.into())?;
writer.start("level_count")?;
#[expect(
clippy::cast_possible_truncation,
reason = "level count is bounded by 255"
)]
writer.write_u8(self.level_count() as u8)?;
writer.start("filter_hash_type")?;
writer.write_u8(u8::from(ChecksumType::Xxh3))?;
writer.start("comparator_name")?;
writer.write_all(comparator_name.as_bytes())?;
writer.start("tables")?;
let mut framing_scratch: Vec<u8> = Vec::with_capacity(64);
#[expect(
clippy::cast_possible_truncation,
reason = "there are always less than 256 levels"
)]
writer.write_u8(self.level_count() as u8)?;
for level in self.iter_levels() {
let run_count = u8::try_from(level.len()).map_err(|_| {
crate::Error::from(crate::io::Error::new(
crate::io::ErrorKind::InvalidInput,
"a level holds more runs than a manifest snapshot can record",
))
})?;
writer.write_u8(run_count)?;
for run in level.iter() {
#[expect(
clippy::cast_possible_truncation,
reason = "there are always less than 4 billion tables in a run"
)]
writer.write_u32::<LittleEndian>(run.len() as u32)?;
for table in run.iter() {
framing::write_framed_record(writer, &mut framing_scratch, |payload| {
payload.write_u64::<LittleEndian>(table.id())?;
payload.write_u8(0)?; payload.write_u128::<LittleEndian>(table.checksum().into_u128())?;
payload.write_u64::<LittleEndian>(table.global_seqno())?;
Ok(())
})?;
}
}
}
writer.start("blob_files")?;
#[expect(
clippy::cast_possible_truncation,
reason = "there are always less than 4 billion blob files"
)]
writer.write_u32::<LittleEndian>(self.blob_files.len() as u32)?;
for file in self.blob_files.iter() {
framing::write_framed_record(writer, &mut framing_scratch, |payload| {
payload.write_u64::<LittleEndian>(file.id())?;
payload.write_u8(0)?; payload.write_u128::<LittleEndian>(file.0.checksum.into_u128())?;
Ok(())
})?;
}
writer.start("blob_gc_stats")?;
self.gc_stats.encode_into(writer)?;
writer.start("restrictions")?;
let restricted: Vec<(TableId, crate::UserKey)> = self
.iter_tables()
.filter_map(|t| t.restrict_lower_bound().map(|b| (t.id(), b.clone())))
.collect();
writer.write_u32::<LittleEndian>(
u32::try_from(restricted.len()).map_err(|_| crate::Error::Unrecoverable)?,
)?;
for (id, key) in &restricted {
writer.write_u64::<LittleEndian>(*id)?;
writer.write_u32::<LittleEndian>(
u32::try_from(key.len()).map_err(|_| crate::Error::Unrecoverable)?,
)?;
writer.write_all(key)?;
}
writer.start("blob_restrictions")?;
let blob_restricted: Vec<(crate::vlog::BlobFileId, u64)> = self
.blob_files
.iter()
.filter(|bf| bf.live_data_start() > 0)
.map(|bf| (bf.id(), bf.live_data_start()))
.collect();
writer.write_u32::<LittleEndian>(
u32::try_from(blob_restricted.len()).map_err(|_| crate::Error::Unrecoverable)?,
)?;
for (id, frontier) in &blob_restricted {
writer.write_u64::<LittleEndian>(*id)?;
writer.write_u64::<LittleEndian>(*frontier)?;
}
writer.start("retention_floor")?;
writer.write_u64::<LittleEndian>(self.retention_floor)?;
if !self.dicts.is_empty() {
writer.start("dicts")?;
writer.write_u32::<LittleEndian>(
u32::try_from(self.dicts.len()).map_err(|_| crate::Error::Unrecoverable)?,
)?;
for id in self.dicts.iter() {
writer.write_u32::<LittleEndian>(*id)?;
}
}
Ok(())
}
}