use super::pool::RowBufferPool;
use super::typed::{gather_typed_column, repeat_typed_column};
use crate::executor::streaming::chunk::core::DataChunk;
impl DataChunk {
pub fn multiplicity(&self) -> u64 {
self.multiplicity
}
pub fn set_multiplicity(&mut self, multiplicity: u64) {
debug_assert!(multiplicity >= 1, "multiplicity must be >= 1");
self.multiplicity = multiplicity.max(1);
}
pub fn with_multiplicity(mut self, multiplicity: u64) -> Self {
self.set_multiplicity(multiplicity);
self
}
pub fn logical_len(&self) -> u64 {
(self.visible_count() as u64).saturating_mul(self.multiplicity)
}
pub fn visible_rows(&self) -> impl Iterator<Item = &Vec<linkrs_core::Value>> {
match &self.selection {
Some(indices) => VisibleRows::Selected {
rows: &self.rows,
indices,
pos: 0,
},
None => VisibleRows::All {
rows: &self.rows,
pos: 0,
},
}
}
pub fn expand_visible_rows(&mut self) -> Vec<Vec<linkrs_core::Value>> {
let multiplicity = self.multiplicity;
let visible_indices: Vec<usize> = match &self.selection {
Some(indices) => indices.clone(),
None => (0..self.rows.len()).collect(),
};
let out = match self.selection.take() {
Some(indices) => {
let mut selected = Vec::with_capacity(indices.len());
for &i in &indices {
selected.push(std::mem::take(&mut self.rows[i]));
}
self.rows.clear();
selected
}
None => std::mem::take(&mut self.rows),
};
self.typed_columns = self.typed_columns.take().map(|cols| {
cols.iter()
.map(|col| {
let gathered = gather_typed_column(col, &visible_indices);
if multiplicity <= 1 {
gathered
} else {
repeat_typed_column(&gathered, multiplicity as usize)
}
})
.collect()
});
self.multiplicity = 1;
debug_assert!(
self.selection.is_none() && self.multiplicity == 1,
"expand_visible_rows must leave a flat chunk: \
selection consumed, multiplicity reset to 1"
);
if multiplicity <= 1 || out.is_empty() {
return out;
}
let total = (out.len() as u64).saturating_mul(multiplicity) as usize;
if let Some(stats) = &self.columnar_stats {
stats.record_multiplicity_expanded(total as u64);
}
expand_rows_reusing_buffers(out, multiplicity, total)
}
pub fn with_selection(mut self, indices: Vec<usize>) -> Self {
debug_assert!(indices.is_sorted() && indices.windows(2).all(|w| w[0] < w[1]));
debug_assert!(indices.last().is_none_or(|&i| i < self.rows.len()));
self.selection = Some(indices);
self
}
pub fn selection(&self) -> Option<&[usize]> {
self.selection.as_deref()
}
pub fn visible_count(&self) -> usize {
self.selection
.as_ref()
.map(Vec::len)
.unwrap_or(self.rows.len())
}
pub fn visible_indices(&self) -> Vec<usize> {
match &self.selection {
Some(indices) => indices.clone(),
None => (0..self.rows.len()).collect(),
}
}
pub fn is_visible(&self, idx: usize) -> bool {
match &self.selection {
None => idx < self.rows.len(),
Some(indices) => indices.binary_search(&idx).is_ok(),
}
}
pub fn take_selection(&mut self) -> Option<Vec<usize>> {
self.selection.take()
}
pub fn expand_multiplicity_in_place(&mut self) -> bool {
if self.multiplicity <= 1 {
return false;
}
let multiplicity = self.multiplicity;
let visible_indices: Vec<usize> = match &self.selection {
Some(indices) => indices.clone(),
None => (0..self.rows.len()).collect(),
};
let taken: Vec<Vec<linkrs_core::Value>> = match self.selection.take() {
Some(indices) => {
let mut selected = Vec::with_capacity(indices.len());
for &i in &indices {
selected.push(std::mem::take(&mut self.rows[i]));
}
self.rows.clear();
selected
}
None => std::mem::take(&mut self.rows),
};
self.typed_columns = self.typed_columns.take().map(|cols| {
cols.iter()
.map(|col| {
let gathered = gather_typed_column(col, &visible_indices);
repeat_typed_column(&gathered, multiplicity as usize)
})
.collect()
});
self.multiplicity = 1;
debug_assert!(
self.selection.is_none() && self.multiplicity == 1,
"expand_multiplicity_in_place must leave a flat chunk: \
selection consumed, multiplicity reset to 1"
);
if taken.is_empty() {
return true;
}
let total = (taken.len() as u64).saturating_mul(multiplicity) as usize;
self.rows = expand_rows_reusing_buffers(taken, multiplicity, total);
if let Some(stats) = &self.columnar_stats {
stats.record_multiplicity_expanded(total as u64);
}
true
}
pub fn normalize_for_opaque(&mut self, op: &'static str) -> bool {
let expanded = self.expand_multiplicity_in_place();
let materialized = self.materialize_selection_by(op);
expanded || materialized
}
pub fn materialize_selection_by(&mut self, op: &'static str) -> bool {
let did = self.materialize_selection_inner();
if did {
if let Some(stats) = &self.columnar_stats {
stats.record_selection_materialized_by(op);
}
}
did
}
fn materialize_selection_inner(&mut self) -> bool {
let Some(indices) = self.selection.take() else {
return false;
};
let mut selected = Vec::with_capacity(indices.len());
for &i in &indices {
selected.push(std::mem::take(&mut self.rows[i]));
}
self.rows = selected;
self.typed_columns = self.typed_columns.as_ref().map(|cols| {
cols.iter()
.map(|col| gather_typed_column(col, &indices))
.collect()
});
true
}
pub fn take_indices(&mut self, indices: &[usize]) -> Self {
let layout = std::sync::Arc::clone(&self.layout);
let schema = std::sync::Arc::clone(&self.schema);
let mut selected = Vec::with_capacity(indices.len());
for &i in indices {
selected.push(std::mem::take(&mut self.rows[i]));
}
let typed_columns = self.typed_columns.as_ref().map(|cols| {
cols.iter()
.map(|col| gather_typed_column(col, indices))
.collect()
});
Self {
rows: selected,
typed_columns,
columnar_build_deferred: self.columnar_build_deferred,
selection: None,
multiplicity: self.multiplicity,
schema,
layout,
memory_reservation: self.memory_reservation.take(),
columnar_stats: self.columnar_stats.clone(),
}
}
pub fn slice(&mut self, start: usize, end: usize) -> Self {
assert!(end <= self.rows.len(), "slice end out of bounds");
let layout = std::sync::Arc::clone(&self.layout);
let schema = std::sync::Arc::clone(&self.schema);
let mut selected = Vec::with_capacity(end - start);
for i in start..end {
selected.push(std::mem::take(&mut self.rows[i]));
}
let indices: Vec<usize> = (start..end).collect();
let typed_columns = self.typed_columns.as_ref().map(|cols| {
cols.iter()
.map(|col| gather_typed_column(col, &indices))
.collect()
});
Self {
rows: selected,
typed_columns,
columnar_build_deferred: self.columnar_build_deferred,
selection: None,
multiplicity: self.multiplicity,
schema,
layout,
memory_reservation: self.memory_reservation.take(),
columnar_stats: self.columnar_stats.clone(),
}
}
}
fn expand_rows_reusing_buffers(
source: Vec<Vec<linkrs_core::Value>>,
multiplicity: u64,
total: usize,
) -> Vec<Vec<linkrs_core::Value>> {
debug_assert!(multiplicity > 1, "pool path is only for real expansion");
debug_assert!(!source.is_empty(), "empty source needs no expansion");
let round = source.len();
let mut expanded = Vec::with_capacity(total.min(1 << 22));
expanded.extend(source);
let mut recycled = RowBufferPool::acquire_rows(total.saturating_sub(round));
for _ in 1..multiplicity {
for i in 0..round {
let mut buf = recycled.pop().unwrap_or_default();
buf.extend(expanded[i].iter().cloned());
expanded.push(buf);
}
if expanded.len() >= (1 << 22) {
expanded.reserve(total.saturating_sub(expanded.len()).min(1 << 22));
}
}
expanded
}
enum VisibleRows<'a> {
All {
rows: &'a [Vec<linkrs_core::Value>],
pos: usize,
},
Selected {
rows: &'a [Vec<linkrs_core::Value>],
indices: &'a [usize],
pos: usize,
},
}
impl<'a> Iterator for VisibleRows<'a> {
type Item = &'a Vec<linkrs_core::Value>;
fn next(&mut self) -> Option<Self::Item> {
match self {
VisibleRows::All { rows, pos } => {
let row = rows.get(*pos)?;
*pos += 1;
Some(row)
}
VisibleRows::Selected { rows, indices, pos } => {
let &i = indices.get(*pos)?;
*pos += 1;
rows.get(i)
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = match self {
VisibleRows::All { rows, pos } => rows.len().saturating_sub(*pos),
VisibleRows::Selected { indices, pos, .. } => indices.len().saturating_sub(*pos),
};
(remaining, Some(remaining))
}
}
impl ExactSizeIterator for VisibleRows<'_> {}
#[cfg(test)]
mod tests {
use super::super::core::DataChunk;
use crate::executor::streaming::slot::SlotLayout;
use linkrs_core::Value;
use std::sync::Arc;
fn layout1() -> Arc<SlotLayout> {
Arc::new(SlotLayout::from_names(&["a".to_string()]))
}
#[test]
fn opaque_boundary_expands_multiplicity() {
let rows = vec![vec![Value::Int(1)], vec![Value::Int(2)]];
let mut chunk = DataChunk::new_with_layout(rows, layout1()).with_selection(vec![0, 1]);
chunk.set_multiplicity(3);
assert_eq!(chunk.logical_len(), 6);
assert!(chunk.normalize_for_opaque("Sort"));
assert_eq!(chunk.multiplicity(), 1);
assert!(chunk.selection().is_none());
assert_eq!(chunk.rows.len(), 6);
assert_eq!(
chunk.rows,
vec![
vec![Value::Int(1)],
vec![Value::Int(2)],
vec![Value::Int(1)],
vec![Value::Int(2)],
vec![Value::Int(1)],
vec![Value::Int(2)],
]
);
}
#[test]
fn opaque_boundary_noop_when_flat() {
let rows = vec![vec![Value::Int(1)]];
let mut chunk = DataChunk::new_with_layout(rows, layout1());
assert!(!chunk.normalize_for_opaque("Sort"));
assert_eq!(chunk.rows.len(), 1);
}
#[test]
fn expand_multiplicity_drops_hidden_rows() {
let rows = vec![
vec![Value::Int(1)],
vec![Value::Int(2)],
vec![Value::Int(3)],
];
let mut chunk = DataChunk::new_with_layout(rows, layout1()).with_selection(vec![0, 2]);
chunk.set_multiplicity(2);
assert!(chunk.expand_multiplicity_in_place());
assert_eq!(chunk.rows.len(), 4);
assert_eq!(
chunk.rows,
vec![
vec![Value::Int(1)],
vec![Value::Int(3)],
vec![Value::Int(1)],
vec![Value::Int(3)],
]
);
}
#[test]
fn multiplicity_expansion_runs_on_pooled_buffers() {
use super::super::pool::{RowBufferPool, MAX_POOLED_ROWS};
RowBufferPool::clear_for_test();
let mut spare: Vec<Vec<Value>> = (0..(MAX_POOLED_ROWS + 8))
.map(|i| vec![Value::Int(i as i32)])
.collect();
RowBufferPool::release_rows(&mut spare);
let rows = vec![vec![Value::Int(7)], vec![Value::Int(8)]];
let mut chunk = DataChunk::new_with_layout(rows, layout1());
chunk.set_multiplicity(3);
assert!(chunk.expand_multiplicity_in_place());
assert_eq!(
chunk.rows,
vec![
vec![Value::Int(7)],
vec![Value::Int(8)],
vec![Value::Int(7)],
vec![Value::Int(8)],
vec![Value::Int(7)],
vec![Value::Int(8)],
]
);
assert!(RowBufferPool::pool_len() <= MAX_POOLED_ROWS);
RowBufferPool::clear_for_test();
}
}