#[doc(hidden)]
pub mod seed;
use crate::VectorExecutionReport;
mod text;
use hawdb_core::{HawDBError, Result, RuntimeTaskContext};
use hawdb_plan_cypher::VectorPhysicalPlan;
use std::collections::BTreeMap;
use std::mem::size_of;
use std::num::NonZeroUsize;
pub use text::{TextSeedExecutionOutput, TextSeedExecutionRequest, TextSeedExecutionRow};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExternalReadResultBudget {
pub max_rows: usize,
pub max_memory_bytes: NonZeroUsize,
}
#[derive(Debug, Clone, Copy)]
pub struct ExternalReadResourceContract<'a> {
pub priority: u8,
pub max_parallelism: NonZeroUsize,
pub max_working_memory_bytes: NonZeroUsize,
pub result: ExternalReadResultBudget,
pub task_context: Option<&'a RuntimeTaskContext>,
}
impl ExternalReadResourceContract<'_> {
pub fn checkpoint(&self) -> Result<()> {
match self.task_context {
Some(task_context) => task_context.checkpoint().map_err(|reason| {
HawDBError::Execution(format!("external read task stopped: {reason}"))
}),
None => Ok(()),
}
}
pub fn reserved_memory_bytes(&self) -> usize {
self.max_working_memory_bytes
.get()
.saturating_add(self.result.max_memory_bytes.get())
}
}
pub struct VectorSeedExecutionRequest<'a> {
pub embedding: &'a [f32],
pub metadata_filters: &'a BTreeMap<String, String>,
pub vector_plan: &'a VectorPhysicalPlan,
pub resources: ExternalReadResourceContract<'a>,
}
pub struct VectorSeedExecutionRow {
pub id: String,
pub external_id: Option<String>,
pub score: f64,
}
pub struct VectorSeedExecutionOutput {
pub rows: Vec<VectorSeedExecutionRow>,
pub report: VectorExecutionReport,
}
impl VectorSeedExecutionOutput {
pub fn estimated_memory_bytes(&self) -> usize {
self.rows
.capacity()
.saturating_mul(size_of::<VectorSeedExecutionRow>())
.saturating_add(self.rows.iter().fold(0usize, |total, row| {
total.saturating_add(row.id.capacity()).saturating_add(
row.external_id
.as_ref()
.map_or(0, |external_id| external_id.capacity()),
)
}))
}
pub fn validate_result_budget(&self, budget: ExternalReadResultBudget) -> Result<()> {
if self.rows.len() > budget.max_rows {
return Err(HawDBError::read_budget_exceeded(
hawdb_core::ReadBudgetResource::Rows,
budget.max_rows,
format!(
"external vector read returned {} rows, exceeding result row budget {}",
self.rows.len(),
budget.max_rows
),
));
}
let memory_bytes = self.estimated_memory_bytes();
if memory_bytes > budget.max_memory_bytes.get() {
return Err(HawDBError::Execution(format!(
"external vector read returned {memory_bytes} estimated bytes, exceeding result memory budget {}",
budget.max_memory_bytes
)));
}
Ok(())
}
}
pub trait ExternalReadOperator {
fn execute_text_seed(
&mut self,
request: TextSeedExecutionRequest<'_>,
) -> Result<TextSeedExecutionOutput> {
request.resources.checkpoint()?;
Err(HawDBError::Execution(
"text search capability is unavailable without a text projection".into(),
))
}
fn execute_vector_seed(
&mut self,
request: VectorSeedExecutionRequest<'_>,
) -> Result<VectorSeedExecutionOutput>;
}
#[doc(hidden)]
pub struct NoExternalReadOperator;
impl ExternalReadOperator for NoExternalReadOperator {
fn execute_vector_seed(
&mut self,
_request: VectorSeedExecutionRequest<'_>,
) -> Result<VectorSeedExecutionOutput> {
Err(HawDBError::Execution(
"vector search capability is unavailable without a search projection".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
VectorCompressionMode, VectorExecutionBackend, VectorFallbackReasonCode, VectorScoreSource,
};
use hawdb_core::RuntimeCancellationToken;
use hawdb_plan_cypher::VectorCandidateSource;
pub(super) fn empty_report() -> VectorExecutionReport {
VectorExecutionReport {
backend: VectorExecutionBackend::ScalarFlat,
compression_mode: VectorCompressionMode::Disabled,
candidate_source: VectorCandidateSource::Scalar,
backend_selection_reason: None,
estimated_raw_vector_bytes: None,
filter_selectivity_per_million: None,
candidate_score_source: VectorScoreSource::RawVector,
final_score_source: VectorScoreSource::RawVector,
generated_candidate_count: 0,
descriptor_pruned_count: 0,
scalar_filtered_count: 0,
residual_filtered_count: 0,
candidate_scan_rounds: 0,
reranked_candidate_count: 0,
returned_count: 0,
raw_vector_bytes_read: 0,
candidate_scan_metrics: None,
index_covered_document_count: None,
index_candidate_document_count: None,
index_coverage_complete: None,
fallback_reason_codes: Vec::<VectorFallbackReasonCode>::new(),
}
}
#[test]
fn external_vector_output_fails_closed_on_row_and_memory_budgets() {
let output = VectorSeedExecutionOutput {
rows: vec![VectorSeedExecutionRow {
id: "memory-1".to_string(),
external_id: Some("external-1".to_string()),
score: 1.0,
}],
report: empty_report(),
};
let row_error = output
.validate_result_budget(ExternalReadResultBudget {
max_rows: 0,
max_memory_bytes: NonZeroUsize::new(1024).unwrap(),
})
.unwrap_err();
assert!(matches!(row_error, HawDBError::ReadBudgetExceeded(cause)
if cause.resource == hawdb_core::ReadBudgetResource::Rows && cause.limit == 0));
let memory_error = output
.validate_result_budget(ExternalReadResultBudget {
max_rows: 1,
max_memory_bytes: NonZeroUsize::MIN,
})
.unwrap_err();
assert!(matches!(memory_error, HawDBError::Execution(_)));
}
#[test]
fn external_read_contract_observes_cancellation() {
let cancellation = RuntimeCancellationToken::new();
let task_context = RuntimeTaskContext::without_deadline(cancellation.clone());
let contract = ExternalReadResourceContract {
priority: 128,
max_parallelism: NonZeroUsize::MIN,
max_working_memory_bytes: NonZeroUsize::MIN,
result: ExternalReadResultBudget {
max_rows: 1,
max_memory_bytes: NonZeroUsize::MIN,
},
task_context: Some(&task_context),
};
assert!(contract.checkpoint().is_ok());
assert!(cancellation.cancel());
assert!(contract.checkpoint().is_err());
}
}