use std::collections::BTreeMap;
use std::sync::Arc;
use arrow::datatypes::{DataType, Schema};
use arrow::record_batch::{RecordBatch, RecordBatchReader};
use super::record_lazy;
use crate::distinct::{SealedState, anti_join_exact_states};
use crate::{
CancellationToken, ErrorCode, ExactState, FingerprintOptions, FingerprintVersion,
ProofFrameError, ReferenceNullPolicyAst, ReferencePlan, ResourceAccount, ResourceLimits,
ValidationState, ValueKind, ValueRef, encoding::V2FingerprintState,
};
#[derive(Debug, Clone, serde::Serialize)]
pub struct ReferenceOutcome {
pub name: String,
pub reference: String,
pub reference_fingerprint: String,
pub reference_rows: u64,
pub reference_distinct_keys: u64,
pub checked_distinct_keys: u64,
pub missing_distinct_keys: u64,
}
#[derive(Default)]
pub struct ReferenceBindings {
readers: BTreeMap<String, Box<dyn RecordBatchReader + Send>>,
}
impl std::fmt::Debug for ReferenceBindings {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("ReferenceBindings")
.field("names", &self.readers.keys().collect::<Vec<_>>())
.finish()
}
}
impl ReferenceBindings {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn insert(
&mut self,
name: impl Into<String>,
reader: Box<dyn RecordBatchReader + Send>,
) -> Option<Box<dyn RecordBatchReader + Send>> {
self.readers.insert(name.into(), reader)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.readers.is_empty()
}
fn take(&mut self, name: &str) -> Option<Box<dyn RecordBatchReader + Send>> {
self.readers.remove(name)
}
fn remaining(&self) -> Vec<&str> {
self.readers.keys().map(String::as_str).collect()
}
}
pub(super) struct PreparedReference {
plan: ReferencePlan,
reference: Arc<SealedState>,
fingerprint: String,
reference_rows: u64,
}
pub(super) struct ReferenceState {
entries: Vec<ReferenceEntry>,
_directory: Option<tempfile::TempDir>,
}
struct ReferenceEntry {
plan: ReferencePlan,
local: ExactState,
scratch: Vec<u8>,
}
pub(super) fn prepare(
plans: &[ReferencePlan],
mut bindings: ReferenceBindings,
local_schema: &Schema,
account: &ResourceAccount,
limits: ResourceLimits,
cancellation: &CancellationToken,
) -> Result<Vec<PreparedReference>, ProofFrameError> {
let mut prepared = Vec::with_capacity(plans.len());
let mut scanned = BTreeMap::<String, (Arc<SealedState>, String, u64)>::new();
for plan in plans {
let key = reference_cache_key(plan);
if !scanned.contains_key(&key) {
let Some(reader) = bindings.take(plan.reference()) else {
return Err(ProofFrameError::contract(
ErrorCode::ReferenceUnbound,
format!(
"Reference rule `{}` needs a dataset bound to `{}`",
plan.name(),
plan.reference()
),
Some(format!("$.dataset_rules.references[{}]", plan.name())),
));
};
let (sealed, fingerprint, rows) =
scan_reference(plan, reader, local_schema, account, limits, cancellation)?;
scanned.insert(key.clone(), (Arc::new(sealed), fingerprint, rows));
}
let (reference, fingerprint, reference_rows) = scanned
.get(&key)
.expect("the reference was just scanned or already cached");
prepared.push(PreparedReference {
plan: plan.clone(),
reference: Arc::clone(reference),
fingerprint: fingerprint.clone(),
reference_rows: *reference_rows,
});
}
let unused = bindings.remaining();
if !unused.is_empty() {
return Err(ProofFrameError::contract(
ErrorCode::ReferenceUnbound,
format!(
"No reference rule uses the bound dataset(s): {}",
unused.join(", ")
),
None,
));
}
Ok(prepared)
}
fn reference_cache_key(plan: &ReferencePlan) -> String {
let mut key = String::from(plan.reference());
for column in plan.reference_columns() {
key.push('\u{1f}');
key.push_str(column);
}
key
}
fn scan_reference(
plan: &ReferencePlan,
reader: Box<dyn RecordBatchReader + Send>,
local_schema: &Schema,
account: &ResourceAccount,
limits: ResourceLimits,
cancellation: &CancellationToken,
) -> Result<(SealedState, String, u64), ProofFrameError> {
let schema = reader.schema();
let indexes = resolve_reference_key(plan, local_schema, schema.as_ref())?;
let directory = tempfile::TempDir::new()?;
let mut state = ExactState::new_with_cancellation(
ValueKind::Bytes,
account.child(limits.max_memory_bytes, limits.max_temp_bytes),
Some(directory.path().to_path_buf()),
None,
cancellation.clone(),
)?;
let mut fingerprint = V2FingerprintState::new(
schema.as_ref(),
&FingerprintOptions::new(FingerprintVersion::V2),
)?;
let mut scratch = Vec::with_capacity(indexes.len().saturating_mul(24));
let mut rows = 0_u64;
for batch in reader {
cancellation.check()?;
let batch = batch?;
if batch.schema().as_ref() != schema.as_ref() {
return Err(ProofFrameError::SchemaMismatch(format!(
"reference dataset `{}` changed schema while it was scanned",
plan.reference()
)));
}
fingerprint.update(&batch)?;
for row in 0..batch.num_rows() {
if encode_key(&batch, &indexes, row, &mut scratch)? {
state.insert(ValueRef::Bytes(&scratch), rows + row as u64)?;
}
}
rows += batch.num_rows() as u64;
}
let sealed = SealedState::seal(state, directory)?;
Ok((sealed, fingerprint.finish().to_tagged_string(), rows))
}
fn resolve_reference_key(
plan: &ReferencePlan,
local_schema: &Schema,
reference_schema: &Schema,
) -> Result<Vec<usize>, ProofFrameError> {
plan.reference_columns()
.iter()
.enumerate()
.map(|(ordinal, column)| {
let index = reference_schema.index_of(column).map_err(|_| {
ProofFrameError::contract(
ErrorCode::MissingColumn,
format!(
"Reference dataset `{}` has no column `{column}` for rule `{}`",
plan.reference(),
plan.name()
),
None,
)
})?;
let local = local_schema.field(plan.columns()[ordinal]).data_type();
let remote = reference_schema.field(index).data_type();
if !types_match(local, remote) {
return Err(ProofFrameError::contract(
ErrorCode::ContractTypeMismatch,
format!(
"Reference rule `{}` pairs `{local}` with `{remote}` at key position {ordinal}",
plan.name()
),
None,
));
}
Ok(index)
})
.collect()
}
fn types_match(local: &DataType, remote: &DataType) -> bool {
match (local, remote) {
(DataType::Timestamp(left, _), DataType::Timestamp(right, _)) => left == right,
_ => local == remote,
}
}
impl ReferenceState {
pub(super) fn new(
prepared: Vec<PreparedReference>,
account: &ResourceAccount,
limits: ResourceLimits,
row_count_hint: Option<u64>,
cancellation: &CancellationToken,
) -> Result<(Self, Vec<PreparedReference>), ProofFrameError> {
if prepared.is_empty() {
return Ok((
Self {
entries: Vec::new(),
_directory: None,
},
prepared,
));
}
let directory = tempfile::TempDir::new()?;
let mut entries = Vec::with_capacity(prepared.len());
for reference in &prepared {
entries.push(ReferenceEntry {
plan: reference.plan.clone(),
local: ExactState::new_with_cancellation(
ValueKind::Bytes,
account.child(limits.max_memory_bytes, limits.max_temp_bytes),
Some(directory.path().to_path_buf()),
row_count_hint,
cancellation.clone(),
)?,
scratch: Vec::with_capacity(reference.plan.columns().len().saturating_mul(24)),
});
}
Ok((
Self {
entries,
_directory: Some(directory),
},
prepared,
))
}
pub(super) fn update(
&mut self,
batch: &RecordBatch,
row_offset: u64,
validation: &mut ValidationState,
) -> Result<(), ProofFrameError> {
for entry in &mut self.entries {
for row in 0..batch.num_rows() {
let global_row = row_offset + row as u64;
if encode_key(batch, entry.plan.columns(), row, &mut entry.scratch)? {
entry
.local
.insert(ValueRef::Bytes(&entry.scratch), global_row)?;
} else if entry.plan.nulls() == ReferenceNullPolicyAst::Reject {
record_lazy(
validation,
"references",
"$dataset",
Some(global_row),
|| format!("Reference rule `{}` rejects null keys", entry.plan.name()),
);
}
}
}
Ok(())
}
pub(super) fn finish(
self,
prepared: Vec<PreparedReference>,
max_samples: usize,
validation: &mut ValidationState,
) -> Result<(Vec<ReferenceOutcome>, super::ExactMetrics), ProofFrameError> {
let mut outcomes = Vec::with_capacity(prepared.len());
let mut metrics = super::ExactMetrics::default();
for (entry, reference) in self.entries.into_iter().zip(prepared) {
let summary = anti_join_exact_states(entry.local, &reference.reference, max_samples)?;
metrics.spill_bytes = metrics.spill_bytes.saturating_add(summary.spill_bytes);
metrics.exact_runs = metrics.exact_runs.saturating_add(summary.runs);
let sampled = summary.samples.len() as u64;
let column = entry
.plan
.fields()
.first()
.map_or("$dataset", |field| field.name().as_str())
.to_string();
for sample in &summary.samples {
record_lazy(validation, "references", &column, Some(sample.row), || {
format!(
"Reference rule `{}` found a key absent from `{}`",
entry.plan.name(),
entry.plan.reference()
)
});
}
validation.violation_count = validation
.violation_count
.saturating_add(summary.missing.saturating_sub(sampled));
outcomes.push(ReferenceOutcome {
name: entry.plan.name().to_string(),
reference: entry.plan.reference().to_string(),
reference_fingerprint: reference.fingerprint,
reference_rows: reference.reference_rows,
reference_distinct_keys: summary.right_distinct,
checked_distinct_keys: summary.left_distinct,
missing_distinct_keys: summary.missing,
});
}
Ok((outcomes, metrics))
}
}
fn encode_key(
batch: &RecordBatch,
indexes: &[usize],
row: usize,
output: &mut Vec<u8>,
) -> Result<bool, ProofFrameError> {
output.clear();
for (ordinal, index) in indexes.iter().enumerate() {
let array = batch.column(*index);
if array.is_null(row) {
return Ok(false);
}
output.extend_from_slice(&(ordinal as u64).to_le_bytes());
super::dataset_state::append_scalar(array.as_ref(), row, output)?;
}
Ok(true)
}
pub(super) fn count_delta_references(
plans: &[crate::RowCountDeltaPlan],
bindings: &mut ReferenceBindings,
cancellation: &CancellationToken,
) -> Result<BTreeMap<String, u64>, ProofFrameError> {
let mut counted = BTreeMap::<String, u64>::new();
for plan in plans {
if counted.contains_key(plan.reference()) {
continue;
}
let Some(reader) = bindings.take(plan.reference()) else {
return Err(ProofFrameError::contract(
ErrorCode::ReferenceUnbound,
format!(
"Row count rule `{}` needs a dataset bound to `{}`",
plan.name(),
plan.reference()
),
Some(format!("$.dataset_rules.row_count_delta[{}]", plan.name())),
));
};
let mut rows = 0u64;
for batch in reader {
cancellation.check()?;
rows = rows.saturating_add(batch?.num_rows() as u64);
}
counted.insert(plan.reference().to_string(), rows);
}
Ok(counted)
}