use crate::table::group_carry::{CarryCandidate, CarryQueue, CarryShape, CarryTarget};
use crate::{InternalValue, UserKey};
pub fn plan(
config: &crate::Config,
rc: &crate::runtime_config::RuntimeConfig,
dest_canonical: usize,
inputs: &[crate::Table],
) -> Option<CarryTarget> {
let blocks = CarryShape::Blocks {
restart_interval: config
.data_block_restart_interval_policy
.get(dest_canonical),
hash_ratio_bits: config
.data_block_hash_ratio_policy
.get(dest_canonical)
.to_bits(),
};
#[cfg(feature = "columnar")]
let shape = if rc.columnar {
CarryShape::Groups(crate::table::meta::GroupShape {
encoding: config.column_encoding_policy.get(dest_canonical),
row_group_size: config.columnar_row_group_size_policy.get(dest_canonical),
page_size: config.columnar_page_size_policy.get(dest_canonical),
})
} else {
blocks
};
#[cfg(not(feature = "columnar"))]
let shape = blocks;
let codec = rc.data_block_compression_policy.get(dest_canonical);
let saves =
!matches!(shape, CarryShape::Blocks { .. }) || codec != crate::CompressionType::None;
let target = CarryTarget {
queue: CarryQueue::default(),
codec,
shape,
};
(saves
&& config.encryption.is_none()
&& !config.page_ecc
&& inputs.iter().any(|table| target.takes(table)))
.then_some(target)
}
pub trait CarrySink {
fn write(&mut self, row: InternalValue) -> crate::Result<()>;
fn carry(&mut self, candidate: &CarryCandidate) -> crate::Result<bool>;
fn carry_pages(
&mut self,
candidate: &CarryCandidate,
emitted: &[InternalValue],
) -> crate::Result<Option<u64>>;
fn transforms_seen(&self) -> u64;
fn continues_last(&self, key: &[u8]) -> bool;
fn settle_transforms(&mut self, seen: u64);
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Carried {
pub(crate) groups: u64,
pub(crate) blocks: u64,
pub(crate) rows: u64,
pub(crate) partial_groups: u64,
pub(crate) bytes: u64,
}
impl core::ops::AddAssign for Carried {
fn add_assign(&mut self, other: Self) {
self.groups += other.groups;
self.blocks += other.blocks;
self.rows += other.rows;
self.partial_groups += other.partial_groups;
self.bytes += other.bytes;
}
}
enum SeenCounts {
Same { count: u64, rows: usize },
Each(alloc::vec::Vec<u64>),
}
impl SeenCounts {
fn push(&mut self, count: u64) {
match self {
Self::Same { count: same, rows } if *same == count => *rows += 1,
Self::Same { count: same, rows } => {
let mut each = alloc::vec![*same; *rows];
each.push(count);
*self = Self::Each(each);
}
Self::Each(each) => each.push(count),
}
}
fn len(&self) -> usize {
match self {
Self::Same { rows, .. } => *rows,
Self::Each(each) => each.len(),
}
}
fn get(&self, at: usize) -> u64 {
match self {
Self::Same { count, .. } => *count,
Self::Each(each) => each.get(at).copied().unwrap_or_default(),
}
}
}
struct Open {
candidate: CarryCandidate,
emitted: alloc::vec::Vec<InternalValue>,
seen: SeenCounts,
last_seen: u64,
held_bytes: u64,
}
impl Open {
fn new<S: CarrySink + ?Sized>(candidate: CarryCandidate, row: InternalValue, sink: &S) -> Self {
let last_seen = sink.transforms_seen();
let mut open = Self {
candidate,
held_bytes: 0,
emitted: alloc::vec::Vec::new(),
seen: SeenCounts::Same {
count: last_seen,
rows: 0,
},
last_seen,
};
open.hold(row, sink);
open
}
fn hold<S: CarrySink + ?Sized>(&mut self, row: InternalValue, sink: &S) {
self.last_seen = sink.transforms_seen();
self.seen.push(self.last_seen);
if !self.candidate.is_block() {
self.held_bytes += crate::table::group_carry::row_bytes(&row);
self.emitted.push(row);
#[cfg(all(test, feature = "columnar"))]
tests::note_held(&self.emitted);
}
}
fn held(&self) -> usize {
self.seen.len()
}
fn overfilled(&self) -> bool {
self.emitted.len() > 2 * self.candidate.rows.len()
|| self.held_bytes > 2 * self.candidate.bytes()
}
}
pub struct CarryMatcher {
queue: CarryQueue,
comparator: crate::comparator::SharedComparator,
open: Option<Open>,
none_below: Option<(usize, Option<UserKey>)>,
carried: Carried,
}
impl CarryMatcher {
pub(crate) fn new(queue: CarryQueue, comparator: crate::comparator::SharedComparator) -> Self {
Self {
queue,
comparator,
open: None,
none_below: None,
carried: Carried::default(),
}
}
#[expect(
clippy::inline_always,
reason = "every row a carrying compaction emits passes here"
)]
#[inline(always)]
pub(crate) fn write<S: CarrySink + ?Sized>(
&mut self,
row: InternalValue,
sink: &mut S,
) -> crate::Result<()> {
if self.open.is_none() && !self.may_cover(&row) {
return sink.write(row);
}
self.write_watched(row, sink)
}
#[inline(never)]
fn write_watched<S: CarrySink + ?Sized>(
&mut self,
row: InternalValue,
sink: &mut S,
) -> crate::Result<()> {
if let Some(mut open) = self.open.take() {
let within = self
.comparator
.compare(&row.key.user_key, open.candidate.last_key())
!= core::cmp::Ordering::Greater;
if within {
if open.candidate.is_block()
&& !open
.candidate
.rows
.get(open.held())
.is_some_and(|read| same_row(read, &row))
{
Self::write_rows(open, sink)?;
return sink.write(row);
}
open.hold(row, sink);
if open.overfilled() {
Self::write_rows(open, sink)?;
} else {
self.open = Some(open);
}
return Ok(());
}
self.resolve(open, sink)?;
}
if self.may_cover(&row)
&& !sink.continues_last(&row.key.user_key)
&& let Some(candidate) = self.take_covering(&row)
&& (!candidate.is_block() || candidate.rows.first().is_some_and(|read| same_row(read, &row)))
{
self.open = Some(Open::new(candidate, row, sink));
return Ok(());
}
sink.write(row)
}
pub(crate) fn finish<S: CarrySink + ?Sized>(mut self, sink: &mut S) -> crate::Result<Carried> {
if let Some(open) = self.open.take() {
self.resolve(open, sink)?;
}
Ok(self.carried)
}
fn resolve<S: CarrySink + ?Sized>(
&mut self,
mut open: Open,
sink: &mut S,
) -> crate::Result<()> {
let candidate = &open.candidate;
let emitted = &open.emitted;
let unchanged = if candidate.is_block() {
open.held() == candidate.rows.len()
} else {
emitted.len() == candidate.rows.len()
&& emitted
.iter()
.zip(candidate.rows.iter())
.all(|(row, read)| same_row(read, row))
};
if unchanged {
if sink.carry(candidate)? {
match candidate.unit {
#[cfg(feature = "columnar")]
crate::table::group_carry::CarryUnit::RowGroup(_) => self.carried.groups += 1,
crate::table::group_carry::CarryUnit::DataBlock(_) => self.carried.blocks += 1,
}
self.carried.rows += candidate.rows.len() as u64;
self.carried.bytes += u64::from(candidate.group.size());
sink.settle_transforms(open.last_seen);
open.candidate.mark_carried();
return Ok(());
}
} else if !candidate.is_block()
&& shares_a_row(&candidate.rows, emitted, &self.comparator)
&& let Some(bytes) = sink.carry_pages(candidate, emitted)?
{
self.carried.partial_groups += 1;
self.carried.bytes += bytes;
sink.settle_transforms(open.last_seen);
open.candidate.mark_carried();
return Ok(());
}
Self::write_rows(open, sink)
}
#[expect(
clippy::inline_always,
reason = "asked of every row a carrying compaction emits"
)]
#[inline(always)]
fn may_cover(&self, row: &InternalValue) -> bool {
let Some((seen, first)) = &self.none_below else {
return true;
};
*seen != self.queue.recorded()
|| first.as_ref().is_some_and(|first| {
self.comparator.compare(&row.key.user_key, first) != core::cmp::Ordering::Less
})
}
fn take_covering(&mut self, row: &InternalValue) -> Option<CarryCandidate> {
use core::cmp::Ordering::{Greater, Less};
let key = &row.key.user_key;
let recorded = self.queue.recorded();
let mut queue = self.queue.lock();
queue.retain(|candidate| self.comparator.compare(candidate.last_key(), key) != Less);
if let Some(at) = queue
.iter()
.position(|candidate| self.comparator.compare(candidate.first_key(), key) != Greater)
{
self.none_below = None;
return queue.remove(at);
}
let first = queue
.iter()
.map(CarryCandidate::first_key)
.min_by(|a, b| self.comparator.compare(a, b))
.cloned();
self.none_below = Some((recorded, first));
None
}
fn write_rows<S: CarrySink + ?Sized>(open: Open, sink: &mut S) -> crate::Result<()> {
if open.candidate.is_block() {
for (at, row) in open.candidate.rows.iter().take(open.held()).enumerate() {
sink.write(row.clone())?;
sink.settle_transforms(open.seen.get(at));
}
return Ok(());
}
for (at, row) in open.emitted.into_iter().enumerate() {
sink.write(row)?;
sink.settle_transforms(open.seen.get(at));
}
Ok(())
}
}
#[cfg(feature = "columnar")]
pub fn align_row_pages(
source: &[InternalValue],
source_pages: &[u32],
emitted: &[InternalValue],
) -> Option<(alloc::vec::Vec<u32>, alloc::vec::Vec<bool>)> {
let mut spans = alloc::vec::Vec::with_capacity(source_pages.len());
let mut start = 0usize;
for &rows in source_pages {
let end = start + rows as usize;
spans.push(source.get(start..end)?);
start = end;
}
let matches_at = |span: &[InternalValue], at: usize| {
emitted
.get(at..at + span.len())
.is_some_and(|rows| rows.iter().zip(span).all(|(row, read)| same_row(read, row)))
};
let mut rows = alloc::vec::Vec::with_capacity(spans.len());
let mut copied = alloc::vec::Vec::with_capacity(spans.len());
let mut at = 0usize;
let mut page = 0usize;
while let Some(span) = spans.get(page) {
if matches_at(span, at) {
rows.push(u32::try_from(span.len()).ok()?);
copied.push(true);
at += span.len();
page += 1;
continue;
}
let (next_page, next_at) = (page + 1..spans.len())
.find_map(|later| {
let later_span = spans.get(later)?;
(at..=emitted.len().checked_sub(later_span.len())?)
.find(|&from| matches_at(later_span, from))
.map(|from| (later, from))
})
.unwrap_or((spans.len(), emitted.len()));
let pages = next_page - page;
let available = next_at - at;
if available < pages {
return None;
}
for share in 0..pages {
let take = available / pages + usize::from(share < available % pages);
rows.push(u32::try_from(take).ok()?);
copied.push(false);
}
at = next_at;
page = next_page;
}
if at < emitted.len() {
let extra = u32::try_from(emitted.len() - at).ok()?;
let last = rows.last_mut()?;
*last = last.checked_add(extra)?;
*copied.last_mut()? = false;
}
copied.contains(&true).then_some((rows, copied))
}
#[cfg(feature = "columnar")]
pub fn changed_pages_fit(
emitted: &[InternalValue],
(row_pages, copied): (&[u32], &[bool]),
limit: u64,
) -> bool {
let mut rest = emitted;
for (&rows, &copied) in row_pages.iter().zip(copied) {
let Some((page, after)) = rest.split_at_checked(rows as usize) else {
return false;
};
if !copied
&& page.len() > 1
&& page
.iter()
.map(crate::table::group_carry::row_bytes)
.sum::<u64>()
> limit
{
return false;
}
rest = after;
}
true
}
#[cfg(all(test, feature = "columnar"))]
#[expect(clippy::indexing_slicing, reason = "test code")]
mod tests;
#[cfg(all(test, feature = "metrics", feature = "lz4"))]
mod block_tests;
fn shares_a_row(
source: &[InternalValue],
emitted: &[InternalValue],
comparator: &crate::comparator::SharedComparator,
) -> bool {
use core::cmp::Ordering::{Equal, Greater, Less};
let (mut source, mut emitted) = (source.iter().peekable(), emitted.iter().peekable());
while let (Some(read), Some(row)) = (source.peek(), emitted.peek()) {
match comparator
.compare(&read.key.user_key, &row.key.user_key)
.then_with(|| row.key.seqno.cmp(&read.key.seqno))
{
Less => {
source.next();
}
Greater => {
emitted.next();
}
Equal => {
if same_row(read, row) {
return true;
}
source.next();
emitted.next();
}
}
}
false
}
fn same_row(read: &InternalValue, row: &InternalValue) -> bool {
read.key.seqno == row.key.seqno
&& read.key.value_type == row.key.value_type
&& same_bytes(&read.key.user_key, &row.key.user_key)
&& same_bytes(&read.value, &row.value)
}
#[inline]
fn same_bytes(a: &[u8], b: &[u8]) -> bool {
(a.as_ptr() == b.as_ptr() && a.len() == b.len()) || a == b
}