use alloc::{sync::Arc, vec::Vec};
use super::util::{LookupFd, ReadCharge, admit_read_block, build_block_transform};
#[cfg(feature = "metrics")]
use super::util::{record_block_decoded, record_block_load_cached, record_block_read};
use super::{Block, BlockHandle, BlockOffset, GlobalTableId};
use crate::coding::Decode;
use crate::fs::FsFile;
use crate::path::Path;
use crate::table::block::{BlockType, EccParams, Header};
use crate::table::column_page::{PageDirectory, PageEntry, PageStamp, PageZones};
use crate::{
Cache, CompressionType, Slice, encryption::EncryptionProvider, file_accessor::FileAccessor,
};
#[cfg(feature = "metrics")]
use crate::metrics::Metrics;
pub struct RowGroupBlocks {
pub directory: Arc<PageDirectory>,
pub row_pages: RowPageSet,
pub pages: Vec<PageSlot>,
}
impl RowGroupBlocks {
#[must_use]
pub fn whole(directory: Arc<PageDirectory>, pages: Vec<Block>) -> Self {
let row_pages = RowPageSet::every(&directory);
let pages = (0..)
.zip(pages)
.map(|(index, block)| PageSlot {
index,
block: Some(block),
})
.collect();
Self {
directory,
row_pages,
pages,
}
}
}
#[cfg(feature = "std")]
pub struct PageDescription {
pub entry: PageEntry,
pub rows: u32,
pub expression: crate::table::columnar::Expression,
pub offsets_len: Option<usize>,
}
pub struct PageSlot {
pub index: usize,
pub block: Option<Block>,
}
#[derive(Clone, Debug)]
pub enum RowPageSet {
Range(core::ops::Range<u16>),
Listed(Vec<u16>),
}
impl RowPageSet {
fn every(directory: &PageDirectory) -> Self {
#[expect(
clippy::cast_possible_truncation,
reason = "a directory holds at most u16::MAX row pages"
)]
let count = directory.row_pages().len() as u16;
Self::Range(0..count)
}
fn iter(&self) -> impl Iterator<Item = u16> + '_ {
let (range, listed) = match self {
Self::Range(range) => (range.clone(), &[][..]),
Self::Listed(listed) => (0..0, listed.as_slice()),
};
range.chain(listed.iter().copied())
}
}
#[derive(Clone, Debug, Default)]
pub enum RowPageSelect<'a> {
#[default]
All,
Range(core::ops::Range<u16>),
Zone {
column_id: u16,
lower: Option<&'a [u8]>,
upper: Option<&'a [u8]>,
},
}
#[derive(Clone, Debug, Default)]
pub struct PageWant<'a> {
pub columns: Option<&'a [u16]>,
pub row_pages: RowPageSelect<'a>,
pub whole: bool,
}
impl<'a> PageWant<'a> {
pub const ALL: Self = Self {
columns: None,
row_pages: RowPageSelect::All,
whole: true,
};
#[must_use]
pub const fn projected(columns: &'a [u16], row_pages: RowPageSelect<'a>) -> Self {
Self {
columns: Some(columns),
row_pages,
whole: false,
}
}
fn zone_column(&self) -> Option<u16> {
match self.row_pages {
RowPageSelect::Zone { column_id, .. } => Some(column_id),
RowPageSelect::All | RowPageSelect::Range(_) => None,
}
}
fn zone_block_column(&self, directory: &PageDirectory) -> Option<u16> {
self.zone_column().filter(|&column_id| {
!directory.head_zones().describes(column_id)
&& directory.zone_block(column_id).is_some()
})
}
fn resolve(
&self,
directory: &PageDirectory,
block_zones: Option<&PageZones>,
) -> crate::Result<Wanted<'a>> {
let row_pages = match &self.row_pages {
RowPageSelect::All => RowPageSet::every(directory),
RowPageSelect::Range(range) => {
if range.start >= range.end || usize::from(range.end) > directory.row_pages().len()
{
return Err(crate::Error::InvalidHeader(
"columnar: row page range outside the group",
));
}
RowPageSet::Range(range.clone())
}
RowPageSelect::Zone {
column_id,
lower,
upper,
} => RowPageSet::Listed(
(0u16..)
.zip(directory.row_pages())
.filter(|&(ordinal, &rows)| {
directory
.head_zones()
.zone(ordinal, *column_id)
.or_else(|| block_zones.and_then(|z| z.zone(ordinal, *column_id)))
.is_none_or(|zone| zone_may_match(zone, rows, *lower, *upper))
})
.map(|(ordinal, _)| ordinal)
.collect(),
),
};
Ok(Wanted {
columns: self.columns,
row_pages,
})
}
}
fn zone_may_match(
zone: crate::table::column_page::Zone<'_>,
rows: u32,
lower: Option<&[u8]>,
upper: Option<&[u8]>,
) -> bool {
if zone.null_count == rows {
return false;
}
if let (Some(lower), Some(max)) = (lower, zone.max)
&& max < lower
{
return false;
}
if let Some(upper) = upper
&& zone.min > upper
{
return false;
}
true
}
struct Wanted<'a> {
columns: Option<&'a [u16]>,
row_pages: RowPageSet,
}
impl Wanted<'_> {
fn slots(&self, directory: &PageDirectory) -> Vec<PageSlot> {
let row_page_count = directory.row_pages().len();
let mut slots = Vec::new();
for (part, first) in directory
.entries()
.iter()
.step_by(row_page_count.max(1))
.enumerate()
{
if self
.columns
.is_some_and(|columns| !columns.contains(&first.id.column_id))
{
continue;
}
let base = part * row_page_count;
slots.extend(self.row_pages.iter().map(|row_page| PageSlot {
index: base + usize::from(row_page),
block: None,
}));
}
slots
}
}
#[derive(Debug)]
pub struct RowPages {
pub ordinals: Vec<u16>,
pub batches: Vec<crate::table::columnar::ColumnBatch>,
}
impl RowPages {
#[must_use]
pub fn row_count(&self) -> u32 {
self.batches.iter().map(|b| b.row_count).sum()
}
pub fn into_batch(self) -> crate::Result<crate::table::columnar::ColumnBatch> {
crate::table::columnar::ColumnBatch::concat(self.batches)
}
}
pub struct GroupRead<'a> {
pub table_id: GlobalTableId,
pub path: &'a Path,
pub file_accessor: &'a FileAccessor,
pub cache: &'a Cache,
pub group: &'a BlockHandle,
pub compression: CompressionType,
pub encryption: Option<&'a dyn EncryptionProvider>,
pub ecc: Option<EccParams>,
#[cfg(zstd_any)]
pub zstd_dict: Option<&'a crate::compression::ZstdDictionary>,
pub heal_hints: Option<&'a crate::heal_hints::HealHints>,
#[cfg(feature = "metrics")]
pub metrics: &'a Metrics,
pub charge: ReadCharge,
pub budget: crate::config::ReadBudget,
pub pace: Option<&'a dyn crate::table::util::ReadPacer>,
pub fd: &'a LookupFd,
}
impl GroupRead<'_> {
pub(crate) fn load(&self, want: &PageWant<'_>) -> crate::Result<RowGroupBlocks> {
if let Some((directory, directory_len)) = self.cache.get_directory(
self.table_id,
self.group.offset(),
self.charge.touches_cache(),
) {
return self.load_from_directory(directory, directory_len, want, true);
}
if !self.charge.touches_cache() {
return self.read_selective(want, None);
}
match self.cache.claim_directory(
self.table_id,
self.group.offset(),
crate::cache::LoadIdentity::Block {
block_type: BlockType::ColumnPageDirectory,
size: self.group.size(),
compression: CompressionType::None,
encrypted: self.encryption.is_some(),
},
) {
crate::cache::DirectoryClaim::Shared(directory, directory_len) => {
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
self.metrics
.block_load_shared
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
self.load_from_directory(directory, directory_len, want, false)
}
crate::cache::DirectoryClaim::Lead(lead) => {
if let Some((directory, directory_len)) =
self.cache
.get_directory(self.table_id, self.group.offset(), true)
{
lead.publish(&directory, directory_len);
return self.load_from_directory(directory, directory_len, want, true);
}
self.read_selective(want, Some(lead))
}
crate::cache::DirectoryClaim::Alone => self.read_selective(want, None),
}
}
fn load_from_directory(
&self,
directory: Arc<PageDirectory>,
directory_len: u32,
want: &PageWant<'_>,
cached: bool,
) -> crate::Result<RowGroupBlocks> {
check_directory_len(indexed_directory_len(self.group)?, directory_len)?;
check_group_extent(self.group, directory_len, &directory)?;
check_head_zones_len(self.group, &directory)?;
self.check_group_tag(&directory)?;
let directory = if want
.zone_column()
.is_some_and(|column_id| directory.lacks_head_zones_for(column_id))
{
let directory = self.with_head_zones(&directory, directory_len, &Slice::empty())?;
if self.charge.touches_cache() {
self.cache.insert_directory(
self.table_id,
self.group.offset(),
Arc::clone(&directory),
directory_len,
);
}
directory
} else {
directory
};
let zones = match want.zone_block_column(&directory) {
Some(column_id) => Some(self.zone_block(
&directory,
directory_len,
column_id,
&Slice::empty(),
true,
)?),
None => None,
};
let wanted = want.resolve(&directory, zones.as_ref())?;
let mut pages = wanted.slots(&directory);
let found = self.fill_cached(&directory, directory_len, &mut pages)?;
self.count_cached(cached, found);
if pages.iter().all(|slot| slot.block.is_some()) {
return Ok(RowGroupBlocks {
directory,
row_pages: wanted.row_pages,
pages,
});
}
let pages = self.fetch_missing(
self.open()?,
&directory,
directory_len,
&Slice::empty(),
pages,
!cached,
)?;
Ok(RowGroupBlocks {
directory,
row_pages: wanted.row_pages,
pages,
})
}
pub(crate) fn load_after(
&self,
earlier: &RowGroupBlocks,
want: &PageWant<'_>,
) -> crate::Result<RowGroupBlocks> {
let directory = Arc::clone(&earlier.directory);
let directory_len = indexed_directory_len(self.group)?;
let zones = match want.zone_block_column(&directory) {
Some(column_id) => Some(self.zone_block(
&directory,
directory_len,
column_id,
&Slice::empty(),
true,
)?),
None => None,
};
let wanted = want.resolve(&directory, zones.as_ref())?;
let mut pages = wanted.slots(&directory);
let mut held = earlier.pages.iter().peekable();
for slot in &mut pages {
while held.next_if(|page| page.index < slot.index).is_some() {}
if let Some(page) = held.next_if(|page| page.index == slot.index) {
slot.block.clone_from(&page.block);
}
}
let found = self.fill_cached(&directory, directory_len, &mut pages)?;
self.count_cached(false, found);
let pages = if pages.iter().all(|slot| slot.block.is_some()) {
pages
} else {
self.fetch_missing(
self.open()?,
&directory,
directory_len,
&Slice::empty(),
pages,
false,
)?
};
Ok(RowGroupBlocks {
directory,
row_pages: wanted.row_pages,
pages,
})
}
fn with_head_zones(
&self,
directory: &Arc<PageDirectory>,
directory_len: u32,
front: &Slice,
) -> crate::Result<Arc<PageDirectory>> {
let Some(head) = directory.head_zone_block() else {
return Ok(Arc::clone(directory));
};
let zones = self.zone_block(directory, directory_len, head.column_id, front, false)?;
Ok(Arc::new(
PageDirectory::clone(directory).with_head_zones(zones)?,
))
}
fn zone_block(
&self,
directory: &PageDirectory,
directory_len: u32,
column_id: u16,
front: &Slice,
cache: bool,
) -> crate::Result<PageZones> {
let Some((after_directory, length)) = directory.zone_block(column_id) else {
return Err(crate::Error::InvalidHeader(
"columnar: the column has no zone block",
));
};
let start = directory_len as usize + after_directory as usize;
let end = start + length as usize;
let handle = BlockHandle::new(BlockOffset(*self.group.offset() + start as u64), length);
if let Some(block) = self.lookup(handle.offset(), BlockType::ColumnZones)? {
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
record_block_load_cached(self.metrics, BlockType::ColumnZones);
}
return directory.decode_zone_block(column_id, &block.data);
}
let bytes = if end <= front.len() {
front.slice(start..end)
} else {
self.check_block_len(length)?;
self.read(self.open()?, start, end - start, BlockType::ColumnZones)?
};
let block = self.verify_and_admit(
&bytes,
&handle,
BlockType::ColumnZones,
CompressionType::None,
false,
)?;
let zones = directory.decode_zone_block(column_id, &block.data)?;
if cache {
self.cache_block(&handle, block);
}
Ok(zones)
}
fn check_group_tag(&self, directory: &PageDirectory) -> crate::Result<()> {
check_group_tag(self.group.group_tag(), directory)
}
fn check_block_len(&self, length: u32) -> crate::Result<()> {
crate::table::block::check_on_disk_size(u64::from(length), self.encryption, self.ecc)
}
fn cache_block(&self, handle: &BlockHandle, block: Block) {
if self.charge.touches_cache() {
self.cache
.insert_block(self.table_id, handle.offset(), block);
}
}
fn read_selective(
&self,
want: &PageWant<'_>,
lead: Option<crate::cache::DirectoryLead>,
) -> crate::Result<RowGroupBlocks> {
let timer = crate::cache::Cache::time_load();
let fd = self.open()?;
let group_len = self.group.size() as usize;
let io_buffer = self.budget.io_buffer() as usize;
let directory_len = indexed_directory_len(self.group)?;
self.check_block_len(directory_len)?;
let head_len = if want.zone_column() == Some(crate::table::columnar::COL_USER_KEY) {
self.group
.row_group()
.map_or(0, |group| group.head_zones_len)
} else {
0
};
if head_len > 0 {
self.check_block_len(head_len)?;
}
let both = directory_len as usize + head_len as usize;
if both > group_len {
return Err(crate::Error::InvalidHeader(
"columnar: page directory overruns its row group",
));
}
let head_end = if both <= io_buffer {
both
} else {
directory_len as usize
};
let prefix = if want.whole && group_len <= io_buffer {
self.read(fd, 0, group_len, BlockType::ColumnPage)?
} else {
self.read(fd, 0, head_end, BlockType::ColumnPageDirectory)?
};
let directory = self.admit_directory(&prefix, directory_len)?;
if self.charge.touches_cache() {
self.cache
.record_directory_load(self.table_id, self.group.offset(), timer);
}
let directory = if want
.zone_column()
.is_some_and(|column_id| directory.lacks_head_zones_for(column_id))
{
self.with_head_zones(&directory, directory_len, &prefix)?
} else {
directory
};
let zones = match want.zone_block_column(&directory) {
Some(column_id) => {
Some(self.zone_block(&directory, directory_len, column_id, &prefix, true)?)
}
None => None,
};
let wanted = want.resolve(&directory, zones.as_ref())?;
let mut pages = wanted.slots(&directory);
if prefix.len() != group_len {
let found = self.fill_cached(&directory, directory_len, &mut pages)?;
self.count_cached(false, found);
}
let pages = self.fetch_missing(
fd,
&directory,
directory_len,
&prefix,
pages,
lead.is_some(),
)?;
if self.charge.touches_cache() {
self.cache.insert_directory(
self.table_id,
self.group.offset(),
Arc::clone(&directory),
directory_len,
);
}
if let Some(lead) = lead {
lead.publish(&directory, directory_len);
}
Ok(RowGroupBlocks {
directory,
row_pages: wanted.row_pages,
pages,
})
}
fn admit_directory(
&self,
front: &Slice,
directory_len: u32,
) -> crate::Result<Arc<PageDirectory>> {
check_directory_len(
directory_len,
Header::decode_from(&mut &front[..])?.on_disk_size_with(self.ecc),
)?;
let handle = BlockHandle::new(self.group.offset(), directory_len);
let bytes = front.get(..directory_len as usize).ok_or_else(|| {
crate::Error::InvalidHeader("columnar: page directory overruns its row group")
})?;
let block = self.verify_and_admit(
bytes,
&handle,
BlockType::ColumnPageDirectory,
CompressionType::None,
false,
)?;
let directory = Arc::new(PageDirectory::decode(&block.data)?);
check_group_extent(self.group, directory_len, &directory)?;
check_head_zones_len(self.group, &directory)?;
self.check_group_tag(&directory)?;
Ok(directory)
}
fn fetch_missing(
&self,
fd: &dyn FsFile,
directory: &PageDirectory,
directory_len: u32,
front: &Slice,
mut pages: Vec<PageSlot>,
with_directory: bool,
) -> crate::Result<Vec<PageSlot>> {
let entries = directory.entries();
let io_buffer = self.budget.io_buffer() as usize;
let leads = self.claim_pages(directory, directory_len, front, &mut pages, with_directory);
let mut leads = leads.into_iter().peekable();
let requests = {
let missing = |i: usize| pages.get(i).is_some_and(|slot| slot.block.is_none());
let follows = |i: usize| {
i.checked_sub(1)
.and_then(|before| pages.get(before))
.zip(pages.get(i))
.is_some_and(|(before, slot)| before.index + 1 == slot.index)
};
let mut requests = Vec::new();
let mut i = 0;
while i < pages.len() {
if !missing(i) {
i += 1;
continue;
}
let first = i;
let mut bytes = 0usize;
while i < pages.len() && missing(i) && (i == first || follows(i)) {
let length = pages
.get(i)
.and_then(|slot| entries.get(slot.index))
.map_or(0, |e| e.length);
self.check_block_len(length)?;
let length = length as usize;
if i > first && bytes + length > io_buffer {
break;
}
bytes += length;
i += 1;
}
requests.push(first..i);
}
requests
};
let pages_at = directory_len as usize + directory.pages_start() as usize;
let page_at = |entry: &PageEntry| pages_at + entry.offset as usize;
let spans = {
let entry_of = |slot: usize| pages.get(slot).and_then(|s| entries.get(s.index));
requests
.iter()
.map(|run| {
let first = entry_of(run.start)?;
let last = entry_of(run.end.checked_sub(1)?)?;
Some((page_at(first), page_at(last) + last.length as usize))
})
.collect::<Option<Vec<_>>>()
.ok_or_else(|| {
crate::Error::InvalidHeader("columnar: page outside its row group")
})?
};
let in_flight = usize::from(self.budget.in_flight());
for (batch, spans) in requests.chunks(in_flight).zip(spans.chunks(in_flight)) {
let beyond: Vec<(usize, usize)> = spans
.iter()
.filter(|&&(start, _)| start >= front.len())
.map(|&(start, end)| (start, end - start))
.collect();
let timer = crate::cache::Cache::time_load();
let mut read = self.read_many(fd, &beyond)?.into_iter();
for (run, &(start, end)) in batch.iter().zip(spans) {
let span = if start >= front.len() {
read.next().ok_or_else(|| {
crate::Error::InvalidHeader("columnar: a request came back missing")
})?
} else {
self.span(fd, front, start, end)?
};
self.admit_pages(
directory,
directory_len,
run.clone(),
start,
&span,
&mut pages,
&mut leads,
)?;
}
if self.charge.touches_cache() {
let pages_from = *self.group.offset()
+ u64::from(directory_len)
+ u64::from(directory.pages_start());
self.cache.record_block_loads(
self.table_id,
batch
.iter()
.zip(spans)
.filter(|&(_, &(start, _))| start >= front.len())
.flat_map(|(run, _)| run.clone())
.filter_map(|slot| pages.get(slot).and_then(|s| entries.get(s.index)))
.map(|entry| BlockOffset(pages_from + u64::from(entry.offset))),
timer,
);
}
}
Ok(pages)
}
fn claim_pages(
&self,
directory: &PageDirectory,
directory_len: u32,
front: &Slice,
pages: &mut [PageSlot],
with_directory: bool,
) -> Vec<(usize, crate::cache::BlockLead<'_>)> {
let mut leads = Vec::new();
if !self.charge.touches_cache() {
return leads;
}
let in_group = directory_len as usize + directory.pages_start() as usize;
let pages_at = *self.group.offset() + in_group as u64;
let entries = directory.entries();
for (at, slot) in pages.iter_mut().enumerate() {
let Some(entry) = entries.get(slot.index).filter(|entry| {
slot.block.is_none()
&& in_group + entry.offset as usize + entry.length as usize > front.len()
}) else {
continue;
};
let offset = BlockOffset(pages_at + u64::from(entry.offset));
let claim = self.cache.claim_page(
self.table_id,
offset,
crate::cache::LoadIdentity::Block {
block_type: BlockType::ColumnPage,
size: entry.length,
compression: self.compression,
encrypted: self.encryption.is_some(),
},
with_directory,
);
match claim {
crate::cache::BlockClaim::Shared(page) => {
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
self.metrics
.block_load_shared
.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
}
slot.block = Some(page);
}
crate::cache::BlockClaim::Lead(lead) => {
match self.cache.get_block(self.table_id, offset) {
Some(page) if page.header.block_type == BlockType::ColumnPage => {
lead.publish(&page);
slot.block = Some(page);
}
_ => leads.push((at, lead)),
}
}
crate::cache::BlockClaim::Alone => {}
}
}
leads
}
#[expect(
clippy::too_many_arguments,
reason = "the run, its bytes, the slots it fills and the leads it publishes"
)]
fn admit_pages(
&self,
directory: &PageDirectory,
directory_len: u32,
run: core::ops::Range<usize>,
start: usize,
span: &Slice,
pages: &mut [PageSlot],
leads: &mut core::iter::Peekable<alloc::vec::IntoIter<(usize, crate::cache::BlockLead)>>,
) -> crate::Result<()> {
let entries = directory.entries();
let pages_at = directory_len as usize + directory.pages_start() as usize;
let page_at = |entry: &PageEntry| pages_at + entry.offset as usize;
let first = run.start;
for (position, slot) in (first..).zip(pages.get_mut(run).into_iter().flatten()) {
let Some(entry) = entries.get(slot.index) else {
continue;
};
let at = page_at(entry) - start;
let Some(bytes) = span.get(at..at + entry.length as usize) else {
return Err(crate::Error::InvalidHeader(
"columnar: page outside its row group",
));
};
let handle = BlockHandle::new(
BlockOffset(*self.group.offset() + page_at(entry) as u64),
entry.length,
);
let page = self.verify_and_admit(
bytes,
&handle,
BlockType::ColumnPage,
self.compression,
true,
)?;
let stamped = page
.data
.first_chunk::<{ PageStamp::LEN }>()
.is_some_and(|stamp| PageStamp::decode(*stamp) == directory.stamp_for(entry));
if !stamped {
return Err(crate::Error::InvalidHeader(
"columnar: page belongs to another row group, column part or row page",
));
}
self.cache_block(&handle, page.clone());
while leads.next_if(|(led, _)| *led < position).is_some() {}
if let Some((_, lead)) = leads.next_if(|(led, _)| *led == position) {
lead.publish(&page);
}
slot.block = Some(page);
}
Ok(())
}
fn read_many(&self, fd: &dyn FsFile, ranges: &[(usize, usize)]) -> crate::Result<Vec<Slice>> {
if ranges.is_empty() {
return Ok(Vec::new());
}
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
for &(_, len) in ranges {
record_block_read(self.metrics, BlockType::ColumnPage, len as u64);
}
}
let regions: Vec<(u64, usize)> = ranges
.iter()
.map(|&(at, len)| (*self.group.offset() + at as u64, len))
.collect();
#[cfg(feature = "std")]
if self.pace.is_some_and(crate::table::util::ReadPacer::active) {
return regions
.iter()
.map(|&(offset, len)| {
let bytes = self.request(|| crate::file::read_exact(fd, offset, len))?;
self.took(len);
Ok(bytes)
})
.collect();
}
let bytes = crate::file::read_exact_many(fd, ®ions)?;
self.took(ranges.iter().map(|&(_, len)| len).sum());
Ok(bytes)
}
fn request<T>(&self, op: impl FnOnce() -> T) -> T {
#[cfg(feature = "std")]
if let Some(pace) = self.pace.filter(|pace| pace.active()) {
let started = std::time::Instant::now();
let out = op();
pace.read_took(started.elapsed());
return out;
}
op()
}
fn span(
&self,
fd: &dyn FsFile,
front: &Slice,
start: usize,
end: usize,
) -> crate::Result<Slice> {
if end <= front.len() {
return Ok(front.slice(start..end));
}
if start >= front.len() {
return self.read(fd, start, end - start, BlockType::ColumnPage);
}
let tail = self.read(fd, front.len(), end - front.len(), BlockType::ColumnPage)?;
Ok(Slice::fused(&front.slice(start..), &tail))
}
fn read(
&self,
fd: &dyn FsFile,
at: usize,
len: usize,
role: BlockType,
) -> crate::Result<Slice> {
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
record_block_read(self.metrics, role, len as u64);
}
#[cfg(not(feature = "metrics"))]
let _ = role;
let bytes =
self.request(|| crate::file::read_exact(fd, *self.group.offset() + at as u64, len))?;
self.took(len);
Ok(bytes)
}
fn took(&self, len: usize) {
if let Some(pace) = self.pace {
pace.read_bytes(len as u64);
}
}
fn fill_cached(
&self,
directory: &PageDirectory,
directory_len: u32,
pages: &mut [PageSlot],
) -> crate::Result<usize> {
let pages_at =
*self.group.offset() + u64::from(directory_len) + u64::from(directory.pages_start());
let entries = directory.entries();
let mut found = 0;
for slot in pages.iter_mut().filter(|slot| slot.block.is_none()) {
if let Some(entry) = entries.get(slot.index) {
let offset = pages_at + u64::from(entry.offset);
slot.block = self.lookup(BlockOffset(offset), BlockType::ColumnPage)?;
found += usize::from(slot.block.is_some());
}
}
Ok(found)
}
fn lookup(&self, offset: BlockOffset, expected: BlockType) -> crate::Result<Option<Block>> {
let block = if self.charge.touches_cache() {
self.cache.get_block(self.table_id, offset)
} else {
self.cache.peek_block(self.table_id, offset)
};
match block {
Some(block) if block.header.block_type != expected => Err(crate::Error::InvalidTag((
"BlockType",
block.header.block_type.into(),
))),
other => Ok(other),
}
}
#[cfg_attr(
not(feature = "metrics"),
expect(clippy::unused_self, reason = "the counters are behind `metrics`")
)]
fn count_cached(&self, directory: bool, pages: usize) {
#[cfg(feature = "metrics")]
if self.charge.is_counted() {
if directory {
record_block_load_cached(self.metrics, BlockType::ColumnPageDirectory);
}
for _ in 0..pages {
record_block_load_cached(self.metrics, BlockType::ColumnPage);
}
}
#[cfg(not(feature = "metrics"))]
let _ = (directory, pages);
}
fn open(&self) -> crate::Result<&dyn FsFile> {
Ok(self.fd.resolve(
self.file_accessor,
&self.table_id,
self.path,
self.charge,
#[cfg(feature = "metrics")]
self.metrics,
)?)
}
fn verify_and_admit(
&self,
bytes: &[u8],
handle: &BlockHandle,
block_type: BlockType,
compression: CompressionType,
with_dict: bool,
) -> crate::Result<Block> {
#[cfg(zstd_any)]
let zstd_dict = if with_dict { self.zstd_dict } else { None };
#[cfg(not(zstd_any))]
let _ = with_dict;
let transform = build_block_transform(
compression,
self.encryption,
self.ecc,
#[cfg(zstd_any)]
zstd_dict,
)?;
let identity = crate::table::block::BlockIdentity {
table_id: self.table_id.table_id(),
block_type,
dict_id: compression.dict_id(),
window_log: 0,
};
let (header, payload, ecc_status, recovery) =
Block::verify_frame(bytes.to_vec(), *handle, identity, &transform)?;
let mut produced = 0;
let data = Block::decompress_payload(&header, payload, &transform, &mut produced);
#[cfg(feature = "metrics")]
record_block_decoded(self.metrics, self.charge, produced);
let data = data?;
admit_read_block(
self.table_id,
self.path,
self.file_accessor,
handle,
block_type,
compression,
self.encryption,
self.ecc,
#[cfg(zstd_any)]
zstd_dict,
self.heal_hints,
#[cfg(feature = "metrics")]
self.metrics,
self.charge,
Block { header, data },
ecc_status,
recovery,
)
}
}
impl RowGroupBlocks {
pub(crate) fn to_row_pages(&self, copied: &mut usize) -> crate::Result<RowPages> {
use crate::table::columnar::{Column, ColumnBatch, DecodeBudget};
let outside = || crate::Error::InvalidHeader("columnar: row page outside the group");
let mut budget = DecodeBudget::default();
let (ordinals, columns) = self.by_row_page(
|_| true,
|page, rows, stamp| Column::decode_page(page, rows, stamp, copied, &mut budget),
|column| column.column_id,
)?;
let mut batches = Vec::with_capacity(ordinals.len());
for (columns, &ordinal) in columns.into_iter().zip(&ordinals) {
let row_count = self.directory.row_page_rows(ordinal).ok_or_else(outside)?;
batches.push(ColumnBatch { row_count, columns });
}
Ok(RowPages { ordinals, batches })
}
pub(crate) fn page_columns(
&self,
want: impl Fn(u16) -> bool,
budget: &mut crate::table::columnar::DecodeBudget,
) -> crate::Result<Vec<crate::table::columnar::RowPageColumns<'_>>> {
use crate::table::columnar::{Column, RowPageColumns};
let (ordinals, columns) = self.by_row_page(
want,
|page, rows, stamp| budget.parsed(Column::parse_page(page, rows, stamp)?),
|column| column.column_id,
)?;
let outside = || crate::Error::InvalidHeader("columnar: row page outside the group");
columns
.into_iter()
.zip(ordinals)
.map(|(columns, ordinal)| {
Ok(RowPageColumns {
ordinal,
start: self.directory.row_page_start(ordinal).ok_or_else(outside)?,
rows: self.directory.row_page_rows(ordinal).ok_or_else(outside)?,
columns,
})
})
.collect()
}
fn check_readable(&self) -> crate::Result<()> {
if self.directory.row_count() == 0 {
return Err(crate::Error::InvalidHeader("columnar: zero-row data block"));
}
let row_page_count = self.directory.row_pages().len();
if self
.directory
.entries()
.iter()
.step_by(row_page_count.max(1))
.any(|entry| entry.id.part != 0)
{
return Err(crate::Error::InvalidHeader(
"columnar: page holds an encoding part this build does not decode",
));
}
Ok(())
}
fn by_row_page<'s, T>(
&'s self,
want: impl Fn(u16) -> bool,
mut read: impl FnMut(&'s crate::Slice, u32, PageStamp) -> crate::Result<T>,
column_of: impl Fn(&T) -> u16,
) -> crate::Result<(Vec<u16>, Vec<Vec<T>>)> {
self.check_readable()?;
let entries = self.directory.entries();
let ordinals: Vec<u16> = self.row_pages.iter().collect();
let per_page = self.pages.len() / ordinals.len().max(1);
let mut columns: Vec<Vec<T>> = ordinals
.iter()
.map(|_| Vec::with_capacity(per_page))
.collect();
let unknown_row_page = || {
crate::Error::InvalidHeader("columnar: page names a row page the group does not have")
};
for slot in &self.pages {
let entry = entries.get(slot.index).ok_or_else(unknown_row_page)?;
if !want(entry.id.column_id) {
continue;
}
let Some(page) = &slot.block else {
return Err(crate::Error::InvalidHeader(
"columnar: a wanted page was not read",
));
};
let rows = self
.directory
.row_page_rows(entry.row_page)
.ok_or_else(unknown_row_page)?;
let column = read(&page.data, rows, self.directory.stamp_for(entry))?;
if column_of(&column) != entry.id.column_id {
return Err(crate::Error::InvalidHeader(
"columnar: page column disagrees with its directory entry",
));
}
let place = match &self.row_pages {
RowPageSet::Range(range) => entry
.row_page
.checked_sub(range.start)
.map(usize::from)
.filter(|&place| place < range.len()),
RowPageSet::Listed(listed) => listed.binary_search(&entry.row_page).ok(),
};
place
.and_then(|place| columns.get_mut(place))
.ok_or_else(unknown_row_page)?
.push(column);
}
Ok((ordinals, columns))
}
#[cfg(feature = "std")]
pub(crate) fn page_encodings(&self) -> crate::Result<Vec<PageDescription>> {
self.check_readable()?;
let entries = self.directory.entries();
let unknown = || {
crate::Error::InvalidHeader("columnar: page names a row page the group does not have")
};
self.pages
.iter()
.map(|slot| {
let entry = *entries.get(slot.index).ok_or_else(unknown)?;
let Some(page) = &slot.block else {
return Err(crate::Error::InvalidHeader(
"columnar: a wanted page was not read",
));
};
let rows = self
.directory
.row_page_rows(entry.row_page)
.ok_or_else(unknown)?;
let column = crate::table::columnar::Column::parse_page(
&page.data,
rows,
self.directory.stamp_for(&entry),
)?;
if column.column_id != entry.id.column_id {
return Err(crate::Error::InvalidHeader(
"columnar: page column disagrees with its directory entry",
));
}
Ok(PageDescription {
entry,
rows,
expression: column.values.describe()?,
offsets_len: column.values.offsets_len(column.type_tag, rows),
})
})
.collect()
}
}
pub fn check_group_tag(
expected: Option<core::num::NonZeroU64>,
directory: &PageDirectory,
) -> crate::Result<()> {
match expected {
Some(tag) if tag.get() == directory.group_tag() => Ok(()),
Some(_) => Err(crate::Error::InvalidHeader(
"columnar: page directory belongs to another row group",
)),
None => Err(crate::Error::InvalidHeader(
"columnar: the index entry names no row group",
)),
}
}
pub fn indexed_directory_len(group: &BlockHandle) -> crate::Result<u32> {
group
.row_group()
.map(|group| group.directory_len.get())
.ok_or_else(|| crate::Error::InvalidHeader("columnar: the index entry names no row group"))
}
pub fn check_directory_len(indexed: u32, found: u32) -> crate::Result<()> {
if indexed == found {
Ok(())
} else {
Err(crate::Error::InvalidHeader(
"columnar: page directory length disagrees with its index entry",
))
}
}
pub fn check_head_zones_len(group: &BlockHandle, directory: &PageDirectory) -> crate::Result<()> {
let Some(indexed) = group.row_group().map(|group| group.head_zones_len) else {
return Err(crate::Error::InvalidHeader(
"columnar: the index entry names no row group",
));
};
if indexed == directory.pages_start() {
Ok(())
} else {
Err(crate::Error::InvalidHeader(
"columnar: head zone block length disagrees with its index entry",
))
}
}
pub fn check_group_extent(
group: &BlockHandle,
directory_len: u32,
directory: &PageDirectory,
) -> crate::Result<()> {
if directory.group_len(directory_len) == Some(group.size()) {
Ok(())
} else {
Err(crate::Error::InvalidHeader(
"columnar: page directory does not fill its row group",
))
}
}