use alloc::vec::Vec;
use core::cell::RefCell;
use crate::blob_tree::FragmentationMap;
use crate::blob_tree::field_row::{RowCell, decode_row, encode_row, row_refs};
use crate::blob_tree::handle::BlobIndirection;
use crate::compaction::stream::DroppedKvCallback;
use crate::{InternalValue, UserKey, ValueType};
#[derive(Default)]
pub struct OwnershipLedger {
frag: FragmentationMap,
key: Option<UserKey>,
kept: Vec<InternalValue>,
orphans: Vec<BlobIndirection>,
ready: Vec<InternalValue>,
released: Vec<crate::vlog::ValueHandle>,
relocating: Vec<crate::vlog::BlobFileId>,
}
impl OwnershipLedger {
pub fn relocating(&mut self, files: impl IntoIterator<Item = crate::vlog::BlobFileId>) {
self.relocating.extend(files);
}
fn is_open_for(&self, key: &[u8]) -> bool {
self.key
.as_ref()
.is_some_and(|open| crate::comparator::same_user_key(open, key))
}
#[expect(
clippy::inline_always,
reason = "every row of a compaction passes here, from two merge loops"
)]
#[inline(always)]
pub fn admit(
&mut self,
item: InternalValue,
write: &mut dyn FnMut(InternalValue) -> crate::Result<()>,
) -> crate::Result<()> {
if !self.is_open_for(&item.key.user_key) {
self.close()?;
}
for row in self.ready.drain(..) {
write(row)?;
}
if !self.kept.is_empty() || item.key.value_type.is_cell_row() {
if self.key.is_none() {
self.key = Some(item.key.user_key.clone());
}
self.kept.push(item);
return Ok(());
}
write(item)
}
fn dropped(&mut self, kv: &InternalValue) -> crate::Result<()> {
if !kv.key.value_type.is_cell_row() {
self.frag.on_dropped(kv);
return Ok(());
}
if !self.is_open_for(&kv.key.user_key) {
self.close()?;
self.key = Some(kv.key.user_key.clone());
}
for (indirection, owned) in row_refs(&kv.value)? {
if owned {
self.orphans.push(indirection);
}
}
Ok(())
}
fn close(&mut self) -> crate::Result<()> {
self.key = None;
let mut kept = core::mem::take(&mut self.kept);
for orphan in core::mem::take(&mut self.orphans) {
if !adopt(&mut kept, &orphan)? {
self.frag.charge(&orphan);
self.released.push(orphan.vhandle);
}
}
if !self.relocating.is_empty() {
for unowned in unowned_refs(&kept, &self.relocating)? {
adopt(&mut kept, &unowned)?;
}
}
self.ready.append(&mut kept);
Ok(())
}
pub fn finish(
mut self,
write: &mut dyn FnMut(InternalValue) -> crate::Result<()>,
) -> crate::Result<(FragmentationMap, Vec<crate::vlog::ValueHandle>)> {
self.close()?;
for row in self.ready.drain(..) {
write(row)?;
}
Ok((self.frag, self.released))
}
}
fn unowned_refs(
rows: &[InternalValue],
files: &[crate::vlog::BlobFileId],
) -> crate::Result<Vec<BlobIndirection>> {
let mut seen: Vec<(BlobIndirection, bool)> = Vec::new();
for row in rows {
if row.key.value_type != ValueType::CellRow {
continue;
}
for (indirection, owned) in row_refs(&row.value)? {
if !files.contains(&indirection.vhandle.blob_file_id) {
continue;
}
match seen
.iter_mut()
.find(|(held, _)| held.vhandle == indirection.vhandle)
{
Some((_, any_owner)) => *any_owner |= owned,
None => seen.push((indirection, owned)),
}
}
}
Ok(seen
.into_iter()
.filter(|&(_, any_owner)| !any_owner)
.map(|(indirection, _)| indirection)
.collect())
}
fn adopt(rows: &mut [InternalValue], orphan: &BlobIndirection) -> crate::Result<bool> {
for row in rows.iter_mut().rev() {
if row.key.value_type != ValueType::CellRow {
continue;
}
let mut cells = decode_row(&row.value)?;
let mut found = false;
for field in &mut cells {
if let RowCell::Ref { indirection, owner } = &mut field.cell
&& indirection.vhandle == orphan.vhandle
{
*owner = true;
found = true;
}
}
if found {
row.value = encode_row(&cells)?.into();
return Ok(true);
}
}
Ok(false)
}
pub struct LedgerHook<'a> {
pub ledger: &'a RefCell<OwnershipLedger>,
pub error: &'a RefCell<Option<crate::Error>>,
}
impl DroppedKvCallback for LedgerHook<'_> {
fn on_dropped(&mut self, kv: &InternalValue) {
if let Err(e) = self.ledger.borrow_mut().dropped(kv) {
self.error.borrow_mut().get_or_insert(e);
}
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used, clippy::indexing_slicing, reason = "test code")]
mod tests;