use std::ops::Range;
use super::batch::Batch;
use super::batch_pool::PooledBuf;
use super::merge::MemBatch;
use super::scatter::{copy_ranges, scatter_copy};
use super::string_heap::{blob_span_key, prorated_blob_cap, walk_heap_spans, BlobCache};
use super::writer::DirectWriter;
use crate::schema::SchemaDescriptor;
use gnitz_wire::wal;
use gnitz_wire::{Regions, TypeCode};
#[derive(Clone, Copy)]
pub struct WireRows<'a> {
batch: &'a Batch,
pick: Pick<'a>,
heap: usize,
}
#[derive(Clone, Copy)]
enum Pick<'a> {
Whole,
Range { start: usize, rows: usize },
Listed { rows: &'a [u32], dedup: bool },
}
impl WireRows<'_> {
pub fn rows(&self) -> usize {
match self.pick {
Pick::Whole => self.batch.count,
Pick::Range { rows, .. } => rows,
Pick::Listed { rows, .. } => rows.len(),
}
}
pub fn byte_size(&self) -> usize {
wal::WAL_HEADER_SIZE + self.rows() * self.batch.schema().row_width() + self.heap
}
pub fn encode(&self, out: &mut [u8]) -> usize {
let b = self.batch;
match self.pick {
Pick::Whole => {
b.debug_verify_dead_heap();
wal::write_block(&b.wire_regions(), b.dead_heap, out)
}
Pick::Range { start, rows } => b
.encode_relocating(rows, self.heap, out, true, |writer| {
let heap_at = b.blob.is_empty().then_some(0);
copy_ranges(&b.as_mem_batch(), heap_at, &[(start, start + rows)], writer);
})
.expect("a sized range fits the bytes its size reserved"),
Pick::Listed { rows, dedup } => b
.encode_listed(rows, self.heap, out, dedup)
.expect("a sized list fits the bytes its size reserved"),
}
}
}
impl Batch {
pub fn wire_whole(&self) -> Option<WireRows<'_>> {
self.wire_rows_within(0, usize::MAX)
}
pub fn wire_rows_within(&self, start: usize, budget: usize) -> Option<WireRows<'_>> {
if start == self.count {
return None;
}
let whole = WireRows {
batch: self,
pick: Pick::Whole,
heap: self.blob.len(),
};
if start == 0 && whole.byte_size() <= budget {
return Some(whole);
}
let (rows, heap) = match self.heap_referencing_slots() {
0 => (self.rows_by_width(start, budget), 0),
slots => self.rows_with_heap(start..self.count, slots, budget),
};
let out = WireRows {
batch: self,
pick: Pick::Range { start, rows },
heap,
};
debug_assert!(
rows <= 1 || out.byte_size() <= budget,
"a run of {rows} rows encodes past {budget}"
);
Some(out)
}
pub fn wire_listed<'a>(&'a self, indices: &'a [u32]) -> Option<WireRows<'a>> {
if indices.is_empty() {
return None;
}
if indices.len() == self.count && indices.iter().zip(0..).all(|(&row, at)| row == at) {
return self.wire_whole();
}
let slots = self.heap_referencing_slots();
let rows = || indices.iter().map(|&row| row as usize);
let copied: usize = rows()
.flat_map(|row| self.long_spans(row, slots))
.map(|span| span.len())
.sum();
let dedup = copied > self.blob.len();
let heap = match dedup {
true => self.rows_with_heap(rows(), slots, usize::MAX).1,
false => copied,
};
Some(WireRows {
batch: self,
pick: Pick::Listed { rows: indices, dedup },
heap,
})
}
fn heap_referencing_slots(&self) -> u64 {
if self.blob.is_empty() {
0
} else {
self.schema().string_payload_slots()
}
}
fn long_spans(&self, row: usize, slots: u64) -> impl Iterator<Item = Range<usize>> + '_ {
gnitz_wire::BitIter(slots)
.filter_map(move |pi| gnitz_wire::german_string_heap(self.get_col_ptr(row, pi, 16), self.blob.len()))
}
fn rows_by_width(&self, start: usize, budget: usize) -> usize {
let slope = self.schema().row_width();
(self.count - start).min((budget.saturating_sub(wal::WAL_HEADER_SIZE) / slope).max(1))
}
fn rows_with_heap(&self, rows: impl ExactSizeIterator<Item = usize>, slots: u64, budget: usize) -> (usize, usize) {
let slope = self.schema().row_width();
let base = wal::WAL_HEADER_SIZE;
let mut seen = BlobCache::new(self.string_cells(rows.len()));
let mut heap = 0usize;
let mut fit = 0usize;
for row in rows {
let row_heap = heap + self.row_heap_cost(row, slots, &mut seen);
if fit > 0 && base + (fit + 1) * slope + row_heap > budget {
break;
}
heap = row_heap;
fit += 1;
}
(fit, heap)
}
fn row_heap_cost(&self, row: usize, slots: u64, seen: &mut BlobCache) -> usize {
let mut cost = 0;
for span in self.long_spans(row, slots) {
let content = &self.blob[span];
if seen.map().insert(blob_span_key(content), 0).is_none() {
cost += content.len();
}
}
cost
}
pub fn wire_regions(&self) -> Regions<'_> {
let mut out = Regions::new();
for r in 0..self.schema().num_regions() {
out.push(self.region_at(r));
}
out.push(&self.blob);
out
}
fn encode_listed(&self, indices: &[u32], heap_bytes: usize, out: &mut [u8], dedup: bool) -> Option<usize> {
let count = indices.len();
self.encode_relocating(count, heap_bytes, out, dedup, |writer| {
scatter_copy(&self.as_mem_batch(), indices, writer);
})
}
fn encode_relocating(
&self,
count: usize,
heap_bytes: usize,
out: &mut [u8],
dedup: bool,
fill: impl FnOnce(&mut DirectWriter<'_>),
) -> Option<usize> {
let fixed = count * self.schema().row_width();
let heap_at = wal::WAL_HEADER_SIZE + fixed;
if heap_at > out.len() {
return None;
}
let mut heap = PooledBuf::with_capacity(heap_bytes);
let mut cache = BlobCache::new(self.string_cells(count));
let block = &mut out[wal::WAL_HEADER_SIZE..heap_at];
let cache = dedup.then_some(&mut cache);
fill(&mut DirectWriter::over(
block,
count,
0,
count,
self.schema(),
&mut heap,
cache,
));
let total = heap_at + heap.len();
out.get_mut(heap_at..total)?.copy_from_slice(&heap);
Some(wal::write_head(out, count, fixed, heap.len(), 0))
}
pub fn encode_scattered_prefix(&self, indices: &[u32], out: &mut [u8]) -> Option<(usize, usize)> {
let width = self.schema().row_width();
let fixed_fit = out.len().saturating_sub(wal::WAL_HEADER_SIZE) / width;
let rows = &indices[..indices.len().min(fixed_fit)];
if rows.is_empty() {
return None;
}
if indices.len() == self.count {
if let Some(whole) = self.wire_whole().filter(|w| w.byte_size() <= out.len()) {
return Some((self.count, whole.encode(out)));
}
}
let slots = self.heap_referencing_slots();
if slots == 0 || wal::WAL_HEADER_SIZE + rows.len() * width + self.blob.len() <= out.len() {
let heap = prorated_blob_cap(self.blob.len(), self.count, rows.len());
if let Some(len) = self.encode_listed(rows, heap, out, true) {
return Some((rows.len(), len));
}
}
let (n, heap) = self.rows_with_heap(rows.iter().map(|&i| i as usize), slots, out.len());
self.encode_listed(&rows[..n], heap, out, true).map(|len| (n, len))
}
pub fn decode_from_wal_block(data: &[u8], schema: &SchemaDescriptor) -> Result<Self, &'static str> {
Ok(Batch::from_mem_batch(&MemBatch::of_wal_block(data, schema)?))
}
pub fn decode_foreign_wal_block(data: &[u8], schema: &SchemaDescriptor) -> Result<Self, &'static str> {
let mut mb = MemBatch::of_wal_block(data, schema)?;
mb.dead_heap = validate_string_cells(&mb)?;
if gnitz_wire::first_not_null_violation(schema.not_null_payload_slots(), mb.null_bmp()).is_some() {
return Err("a null bit on a NOT NULL column");
}
if first_valued_null_cell(&mb).is_some() {
return Err("a non-zero cell under a NULL");
}
Ok(Batch::from_mem_batch(&mb))
}
}
pub(super) fn first_valued_null_cell(mb: &MemBatch<'_>) -> Option<(usize, usize)> {
let schema = mb.schema;
let col = |pi| {
let width = schema.columns[schema.payload_col_idx(pi)].size() as usize;
(mb.col_data(pi, width), width)
};
gnitz_wire::first_valued_null(schema.nullable_payload_slots(), mb.null_bmp(), col)
}
fn validate_string_cells(mb: &MemBatch<'_>) -> Result<usize, &'static str> {
let mut text = Vec::new();
let dead = walk_heap_spans(mb, |cell, tc| {
if !gnitz_wire::german_string_cell_ok(cell, mb.blob) {
return false;
}
if tc == TypeCode::String && !gnitz_wire::german_string_short_ascii(cell) {
let c = gnitz_wire::german_string_content(cell, mb.blob);
if !c.is_ascii() {
text.extend_from_slice(c);
text.push(0);
}
}
true
})
.ok_or("data WAL German string is not in canonical form")?;
simdutf8::basic::from_utf8(&text).map_err(|_| "a STRING cell is not UTF-8")?;
Ok(dead)
}
impl<'a> MemBatch<'a> {
pub fn of_wal_block(data: &'a [u8], schema: &'a SchemaDescriptor) -> Result<Self, &'static str> {
let (count, fixed, blob, dead_heap) = wal::parse_block(data, schema.row_width())?;
Ok(MemBatch {
data: fixed,
schema,
cap: count,
blob,
count,
dead_heap,
})
}
}
#[cfg(test)]
#[path = "tests/batch_wire.rs"]
mod tests;
#[cfg(test)]
#[path = "benches/batch_wire.rs"]
mod bench;