use gnitz_wire::{PkKeys, ReadSpec};
use std::rc::Rc;
use super::SkeletonHydrator;
use crate::relation::{Cut, RelationKind, RelationRegistry};
use gnitz_expr::RowFilter;
use gnitz_zset::algebra::SinkPlan;
use gnitz_zset::repr::{Batch, SkeletonKeys};
use gnitz_zset::schema::SchemaDescriptor;
impl RelationRegistry {
pub fn scan_spec(
&self,
target_id: u64,
spec: ReadSpec,
reply_layout: u64,
mut hydrator: Option<&mut dyn SkeletonHydrator>,
) -> Result<Rc<Batch>, String> {
let entry = self.relation_or_err(target_id)?;
if entry.kind() == RelationKind::Stream {
return Err(format!(
"relation {target_id} is a stream: a stream holds no rows and cannot be read"
));
}
let src_schema = entry.schema();
if spec.is_whole() && !entry.table().has_skeleton_rows() {
check_layout(reply_layout, &src_schema)?;
return Ok(entry.full_scan());
}
let ReadSpec { bound, predicate, sink } = spec;
let (mut source, walk) = self.open_bound(target_id, bound, Cut::Now)?;
let mut filter =
RowFilter::for_read(&predicate, walk.as_ref(), &src_schema).map_err(|e| format!("scan_spec: {e}"))?;
let mut sink = SinkPlan::from_wire(&src_schema, &sink, self.config.adhoc_group_cap)?;
check_layout(reply_layout, sink.output_schema())?;
let chunk_rows = self.config.scan_chunk_rows;
let mut ranges = Vec::new();
for drain_rows in drain_ramp(sink.first_drain(chunk_rows), chunk_rows) {
let mut skeletons = SkeletonKeys::default();
let Some(mut chunk) = source.drain_live_chunk(drain_rows, &mut skeletons) else {
break;
};
if !skeletons.keys.is_empty() {
chunk = hydrate(self, target_id, hydrator.as_deref_mut(), chunk, skeletons)?;
}
filter.ranges(&chunk.as_mem_batch(), &mut ranges);
if sink.push(&chunk, &mut ranges)? {
break;
}
}
Ok(Rc::new(sink.finish()))
}
}
fn hydrate(
registry: &RelationRegistry,
id: u64,
hydrator: Option<&mut (dyn SkeletonHydrator + '_)>,
live: Batch,
mut skeletons: SkeletonKeys,
) -> Result<Batch, String> {
let Some(hydrator) = hydrator else {
return Err(format!(
"relation {id} holds skeleton rows but this process maintains no circuit"
));
};
let keys = PkKeys::from_sorted(live.schema().pk_stride(), std::mem::take(&mut skeletons.keys));
#[cfg(debug_assertions)]
let asked = keys.clone();
let hydrated = hydrator
.hydrate_keys(registry, id, keys)
.map_err(|e| format!("hydrate: view {id}: {e}"))?;
#[cfg(debug_assertions)]
assert_hydration_matches(&hydrated, &asked, &skeletons.coarse);
let schema = *live.schema();
Ok(match live.is_empty() {
true => hydrated,
false => hydrated.merged_consolidated(&live, &schema),
})
}
pub(super) fn check_layout(reply_layout: u64, produced: &SchemaDescriptor) -> Result<(), String> {
if reply_layout != produced.layout_digest() {
return Err("reply schema does not match the output layout".to_string());
}
Ok(())
}
#[cfg(debug_assertions)]
fn assert_hydration_matches(out: &Batch, keys: &PkKeys, coarse: &[i64]) {
use gnitz_zset::repr::pk_group_end;
assert_eq!(keys.len(), coarse.len());
let mut expected = keys.iter().zip(coarse).peekable();
let mut i = 0;
while i < out.len() {
let pk = out.get_pk_bytes(i);
if let Some((key, _)) = expected.next_if(|(key, _)| *key < pk) {
panic!("hydration produced no rows for skeleton key {key:?}");
}
let Some((_, &weight)) = expected.next().filter(|(key, _)| *key == pk) else {
panic!("hydration produced rows for a PK no skeleton row named");
};
let j = pk_group_end(out, i);
let sum = out.as_mem_batch().sum_weights(i, j);
assert_eq!(sum, weight, "hydration weight mismatch for key {pk:?}");
i = j;
}
assert!(
expected.next().is_none(),
"hydration produced no rows for a trailing skeleton key"
);
}
fn drain_ramp(first: usize, chunk_rows: usize) -> impl Iterator<Item = usize> {
let (mut step, mut drained) = (first, 0usize);
std::iter::from_fn(move || {
let rows = step.min(chunk_rows - drained % chunk_rows);
drained = drained.saturating_add(rows);
step = step.saturating_mul(2);
Some(rows)
})
}
#[cfg(test)]
#[path = "tests/scan_spec.rs"]
mod tests;
#[cfg(test)]
#[path = "benches/scan_spec.rs"]
mod bench;