use std::{
collections::{BTreeSet, HashMap},
sync::Arc,
};
use bytes::Bytes;
use futures::{StreamExt, TryStreamExt, stream};
use super::{ReadOnlyCatalog, backfill};
use crate::{
catalog::{
RecentRow, SnapshotId, TableId,
inline::{
InlineBodies, InlineRow, InlineScanKind, materialize_inline_rows,
materialize_locator_rows,
},
projection,
},
error::{Error, Result},
store::{
handle::{ReadHandle, ReadSession},
inline as store_inline,
key::InlineOperation,
proto::InlineChunkValue,
read,
},
transaction::commit,
};
const CHUNK_READ_CONCURRENCY: usize = 8;
fn dense_chunk_references(rows: &mut [InlineRow]) -> Vec<usize> {
let mut dense: HashMap<usize, usize> = HashMap::new();
let mut referenced = Vec::new();
for row in rows {
let next = dense.len();
row.chunk = *dense.entry(row.chunk).or_insert_with(|| {
referenced.push(row.chunk);
next
});
}
referenced
}
pub(crate) struct InlineScan {
session: ReadSession,
table: TableId,
chunks: Vec<store_inline::InlineChunkLocator>,
selected: Vec<InlineRow>,
bodies: InlineBodies,
position: usize,
opened_head: Option<u64>,
}
impl InlineScan {
pub(crate) async fn next_window(
&mut self,
max_rows: usize,
) -> Result<(Vec<InlineRow>, Vec<(InlineOperation, Bytes)>)> {
let end = self
.position
.saturating_add(max_rows)
.min(self.selected.len());
let mut window = self.selected[self.position..end].to_vec();
self.position = end;
let referenced: Vec<store_inline::InlineChunkLocator> = dense_chunk_references(&mut window)
.into_iter()
.map(|index| self.chunks[index])
.collect();
let table = self.table.get();
let handle = self.session.handle();
let chunks = match self.bodies {
InlineBodies::Skip => referenced
.into_iter()
.map(|locator| (locator.operation(), Bytes::new()))
.collect(),
InlineBodies::Fetch => {
let read = stream::iter(referenced.into_iter().map(|locator| async move {
store_inline::read_inline_chunk_locator(handle, table, locator).await
}))
.buffered(CHUNK_READ_CONCURRENCY)
.map_ok(|(operation, chunk)| (operation, chunk.body))
.try_collect()
.await;
match read {
Ok(chunks) => chunks,
Err(error) => return Err(self.classify_window_read(error).await),
}
}
};
Ok((window, chunks))
}
async fn classify_window_read(&self, error: Error) -> Error {
let Some(opened_head) = self.opened_head else {
return error;
};
if !matches!(error, Error::Corruption(_)) {
return error;
}
match commit::read_head_id(self.session.handle()).await {
Ok(head) if head != opened_head => Error::RetryBudgetExhausted(format!(
"inline scan of table {} lost a chunk to a flush that landed mid-scan; \
re-run the read",
self.table
)),
_ => error,
}
}
pub(crate) fn finish(self) {
self.session.finish();
}
}
pub(super) enum InlineRowSource {
Locators(Vec<store_inline::InlineChunkLocator>),
Chunks(Vec<(InlineOperation, InlineChunkValue)>),
}
impl InlineRowSource {
pub(super) async fn resolve_chunks(
self,
handle: ReadHandle<'_>,
table: TableId,
mut selected: Vec<InlineRow>,
) -> Result<(Vec<InlineRow>, Vec<(InlineOperation, InlineChunkValue)>)> {
let referenced = dense_chunk_references(&mut selected);
let chunks = match self {
Self::Locators(locators) => {
stream::iter(referenced.into_iter().map(|index| {
let locator = locators[index];
async move {
store_inline::read_inline_chunk_locator(handle, table.get(), locator).await
}
}))
.buffered(CHUNK_READ_CONCURRENCY)
.try_collect()
.await?
}
Self::Chunks(mut chunks) => referenced
.into_iter()
.map(|index| {
let (operation, chunk) = &mut chunks[index];
(*operation, std::mem::take(chunk))
})
.collect(),
};
Ok((selected, chunks))
}
}
impl ReadOnlyCatalog {
async fn inline_row_source(
&self,
handle: ReadHandle<'_>,
table: TableId,
) -> Result<(InlineRowSource, Vec<InlineRow>)> {
if projection::inline_directory_complete(&self.projections, table.get()) {
let (locators, tombstones) = futures::try_join!(
store_inline::scan_inline_chunk_locators(handle, table.get()),
store_inline::scan_inline_deletes(handle, table.get()),
)?;
let rows = materialize_locator_rows(&locators, &tombstones);
return Ok((InlineRowSource::Locators(locators), rows));
}
let (chunks, tombstones) = futures::try_join!(
store_inline::scan_inline_chunks(handle, table.get()),
store_inline::scan_inline_deletes(handle, table.get()),
)?;
let walked = chunks
.iter()
.filter_map(|(operation, chunk)| {
store_inline::InlineChunkLocator::from_chunk(*operation, chunk).transpose()
})
.collect::<Result<Vec<_>>>()?;
self.verify_inline_directory(handle, table, &walked).await?;
let rows = materialize_inline_rows(&chunks, &tombstones);
Ok((InlineRowSource::Chunks(chunks), rows))
}
async fn inline_chunk_directory(
&self,
handle: ReadHandle<'_>,
table: TableId,
) -> Result<Vec<store_inline::InlineChunkLocator>> {
if projection::inline_directory_complete(&self.projections, table.get()) {
return store_inline::scan_inline_chunk_locators(handle, table.get()).await;
}
let headers = store_inline::scan_inline_chunk_headers(handle, table.get()).await?;
self.verify_inline_directory(handle, table, &headers)
.await?;
Ok(headers)
}
pub(crate) async fn open_inline_scan(
&self,
table_id: u64,
kind: InlineScanKind,
snapshot: u64,
start: u64,
schema_version: Option<u64>,
bodies: InlineBodies,
) -> Result<InlineScan> {
let table = TableId::new(table_id);
let session = self.begin_read().await?;
let selection = read::consistent(session.handle(), || async {
let opened_head = if session.handle().is_isolated() {
None
} else {
Some(commit::read_head_id(session.handle()).await?)
};
let handle = session.handle();
let (chunks, tombstones) = futures::try_join!(
self.inline_chunk_directory(handle, table),
store_inline::scan_inline_deletes(handle, table.get()),
)?;
let rows = materialize_locator_rows(&chunks, &tombstones);
let mut selected = kind.select(&rows, snapshot, start);
if let Some(version) = schema_version {
selected.retain(|row| chunks[row.chunk].schema_version() == Some(version));
}
Ok((chunks, selected, opened_head))
})
.await;
match selection {
Ok((chunks, selected, opened_head)) => Ok(InlineScan {
session,
table,
chunks,
selected,
bodies,
position: 0,
opened_head,
}),
Err(error) => {
session.finish();
Err(error)
}
}
}
#[cfg(test)]
pub(crate) async fn select_inline_rows(
&self,
table_id: u64,
kind: InlineScanKind,
snapshot: u64,
start: u64,
schema_version: Option<u64>,
) -> Result<(Vec<InlineRow>, Vec<(InlineOperation, Bytes)>)> {
let mut scan = self
.open_inline_scan(
table_id,
kind,
snapshot,
start,
schema_version,
InlineBodies::Fetch,
)
.await?;
let window = scan.next_window(usize::MAX).await;
scan.finish();
window
}
pub(super) async fn scan_recent_rows(
&self,
session: &ReadSession,
table: TableId,
at: Option<u64>,
) -> Result<Vec<RecentRow>> {
let handle = session.handle();
self.with_inline_rows(handle, table, async |source, rows| {
let read_at = async {
match at {
Some(_) => Ok(commit::resolve_read_snapshot(handle, at).await?.0),
None => commit::read_head_id(handle).await,
}
};
let read_at = read_at.await?;
let live = InlineScanKind::Table.select(&rows, read_at, 0);
let (live, chunks) = source.resolve_chunks(handle, table, live).await?;
self.recent_rows_from_chunks(handle, table, live, chunks)
.await
})
.await
}
pub(super) async fn with_inline_rows<T>(
&self,
handle: ReadHandle<'_>,
table: TableId,
finish: impl AsyncFn(InlineRowSource, Vec<InlineRow>) -> Result<T>,
) -> Result<T> {
read::consistent(handle, || async {
let (source, rows) = self.inline_row_source(handle, table).await?;
finish(source, rows).await
})
.await
}
pub(super) async fn recent_rows_from_chunks(
&self,
handle: ReadHandle<'_>,
table: TableId,
live: Vec<InlineRow>,
chunks: Vec<(InlineOperation, InlineChunkValue)>,
) -> Result<Vec<RecentRow>> {
let schema_versions = live.iter().filter_map(|row| match &chunks[row.chunk].0 {
InlineOperation::Insert { schema_version, .. } => Some(*schema_version),
_ => None,
});
let schemas: HashMap<u64, Arc<Vec<u8>>> =
backfill::read_inline_schemas(handle, table, schema_versions)
.await?
.into_iter()
.map(|(version, record)| (version, Arc::new(record.to_vec())))
.collect();
let mut bodies: HashMap<usize, Arc<Vec<u8>>> = HashMap::new();
let mut rows = Vec::with_capacity(live.len());
for row in live {
let (operation, chunk) = &chunks[row.chunk];
let InlineOperation::Insert { schema_version, .. } = operation else {
return Err(Error::Corruption(format!(
"inline row {} of table {table} references a non-insert chunk",
row.row_id
)));
};
let arrow_schema = Arc::clone(schemas.get(schema_version).ok_or_else(|| {
Error::Corruption(format!(
"no inline schema for table {table} version {schema_version}"
))
})?);
let chunk_body = Arc::clone(
bodies
.entry(row.chunk)
.or_insert_with(|| Arc::new(chunk.body.to_vec())),
);
rows.push(RecentRow {
row_id: row.row_id,
begin_snapshot: SnapshotId::new(row.begin_snapshot),
schema_version: *schema_version,
offset_in_chunk: row.offset_in_chunk,
chunk_body,
arrow_schema,
});
}
Ok(rows)
}
pub(super) async fn verify_inline_directory(
&self,
handle: ReadHandle<'_>,
table: TableId,
chunks: &[store_inline::InlineChunkLocator],
) -> Result<()> {
if let Some(widest) = chunks
.iter()
.map(|locator| locator.row_id_end() - locator.row_id_start())
.max()
{
projection::note_inline_chunk_width(&self.projections, table.get(), widest);
}
if !handle.is_isolated()
|| projection::format_floor(&self.projections)
< commit::FORMAT_WITH_INLINE_CHUNK_DIRECTORY
{
return Ok(());
}
let directory: BTreeSet<InlineOperation> =
store_inline::scan_inline_chunk_ranges(handle, table.get())
.await?
.into_iter()
.map(|(_, operation)| operation)
.collect();
let walked: BTreeSet<InlineOperation> =
chunks.iter().map(|locator| locator.operation()).collect();
if walked == directory {
projection::note_inline_directory_complete(&self.projections, table.get());
}
Ok(())
}
}